본문 바로가기

명사 美 비격식 (무리 중에서) 아주 뛰어난[눈에 띄는] 사람[것]

Personal/SK 네트웍스 AI 캠프

SK 네트웍스 AI 캠프 - 3_초거대언어모델(LLM) - Day36_자연어 처리를 위한 언어 모델

BERT 

구글이 2018년에 공개한 사전 훈련된 언어 모델

33억 단어에 대해 약 4일간 하기습시킨 언어 모델

위키피디아의 25억개 단어와 북코퍼스의 8억개 레이블이 없는 데이터를 이용하여 사전 훈련됨

레이블이 없는 데이터로 학습한후 레이블이 있는 데이터에서 후가학습하며 미세조정 과정에서 하이퍼파라미터를 다시 조정함. 

튜닝해 영어 잘하는 사람이 비슷한 다른 언어를 쉽게 배우는것과 유사한 원리로 다양한 용도로 활용이 가능하다. 

- 이진분류, 자연어 다중분류, 감성분류, ...

 

BERT-Large는 Base보다 데이터, 모델크기, 파라미터 수가 훨씬 많은 

Hugging Face 홈페이지에서 무료로 다운로드하여 사용할 수 있다 .

약 110 개의 언어에 대한 BERT 모델이 제공되며 한국어 BERT 모델도 제공된다. 

BERT-Base: Encoder L 12, 은닉벡터크기 D 768, self Attention head의 개수 A 12 약 1억 1천만개의 파라미터

BERT-Large: Encoder L 24, 은닉벡터크기 D 1024, self Attention head의 개수 A16 약 3억 4천만개의 파라미터

 

 

 

https://huggingface.co/

 

Hugging Face – The AI community building the future.

We’re on a journey to advance and democratize artificial intelligence through open source and open science.

huggingface.co

 

 

 

 

 

실습코드를 분석해보자 . movie_review_bert.ipynb

 

import

tqdm.auto 반복작업의 진행률을 출력하기 위해 사용하는 라이브러리

transformers BertTokenizer, BertModel Hugging Face에서 BEDRT 토크나이저와 BERT 본체 모델을 불러오기 위한 클래스

# 운영체제 기능을 사용하기 위한 표준 라이브러리입니다.
import os

# URL에서 파일을 다운로드하기 위한 표준 라이브러리입니다.
import urllib.request

# 난수 고정을 위해 사용하는 표준 라이브러리입니다.
import random

# 수치 계산과 배열 처리를 위해 사용하는 라이브러리입니다.
import numpy as np

# 표 형태의 데이터를 읽고 처리하기 위해 사용하는 라이브러리입니다.
import pandas as pd

# 반복 작업의 진행률을 출력하기 위해 사용하는 라이브러리입니다.
from tqdm.auto import tqdm

# PyTorch의 핵심 기능을 사용하기 위한 라이브러리입니다.
import torch

# PyTorch에서 신경망 계층과 손실함수를 만들기 위한 모듈입니다.
import torch.nn as nn

# PyTorch에서 최적화 알고리즘을 사용하기 위한 모듈입니다.
import torch.optim as optim

# PyTorch에서 데이터셋과 미니배치 로더를 만들기 위한 클래스입니다.
from torch.utils.data import Dataset, DataLoader

# 학습 데이터와 검증 데이터를 나누기 위한 scikit-learn 함수입니다.
from sklearn.model_selection import train_test_split

# Hugging Face에서 BERT 토크나이저와 BERT 본체 모델을 불러오기 위한 클래스입니다.
from transformers import BertTokenizer, BertModel

 

 

 

시듸고정

# 실험을 재현하기 위해 사용할 난수 시드 값을 지정합니다.
SEED = 42

# 파이썬 random 모듈의 난수를 고정합니다.
random.seed(SEED)

# NumPy 난수를 고정합니다.
np.random.seed(SEED)

# PyTorch CPU 연산의 난수를 고정합니다.
torch.manual_seed(SEED)

# CUDA GPU를 사용할 수 있는 경우 GPU 연산의 난수도 고정합니다.
torch.cuda.manual_seed_all(SEED)

# 현재 실행 환경에서 CUDA GPU를 사용할 수 있으면 'cuda', 아니면 'cpu'를 선택합니다.
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 선택된 장치를 출력하여 학습이 CPU에서 실행되는지 GPU에서 실행되는지 확인합니다.
print("사용 장치:", device)

 

 

 

데이터불러오기

파일이 없을경우 url에서 urllib.request.urlretrieve()다운로드 하도록 세팅. 

# 네이버 영화 리뷰 훈련 데이터가 저장될 파일 이름을 지정합니다.
train_file = "ratings_train.txt"

# 네이버 영화 리뷰 테스트 데이터가 저장될 파일 이름을 지정합니다.
test_file = "ratings_test.txt"

# 훈련 데이터 다운로드 주소를 지정합니다.
train_url = "https://raw.githubusercontent.com/e9t/nsmc/master/ratings_train.txt"

# 테스트 데이터 다운로드 주소를 지정합니다.
test_url = "https://raw.githubusercontent.com/e9t/nsmc/master/ratings_test.txt"

# 훈련 데이터 파일이 현재 폴더에 없을 때만 다운로드합니다.
if not os.path.exists(train_file):
    # URL에서 훈련 데이터 파일을 다운로드하여 ratings_train.txt로 저장합니다.
    urllib.request.urlretrieve(train_url, filename=train_file)

# 테스트 데이터 파일이 현재 폴더에 없을 때만 다운로드합니다.
if not os.path.exists(test_file):
    # URL에서 테스트 데이터 파일을 다운로드하여 ratings_test.txt로 저장합니다.
    urllib.request.urlretrieve(test_url, filename=test_file)

# 다운로드가 완료되었음을 출력합니다.
print("데이터 다운로드 확인 완료")

 

 

pd.read_table 훈련용 텍스트 파일을 pandas dataframe으로 읽고 데이터를 확인한다. 

# 탭으로 구분된 훈련용 텍스트 파일을 pandas DataFrame으로 읽습니다.
train_data = pd.read_table(train_file)

# 탭으로 구분된 테스트용 텍스트 파일을 pandas DataFrame으로 읽습니다.
test_data = pd.read_table(test_file)

# 훈련용 리뷰 개수를 출력합니다.
print("훈련용 리뷰 개수:", len(train_data))

# 테스트용 리뷰 개수를 출력합니다.
print("테스트용 리뷰 개수:", len(test_data))

# 훈련 데이터의 상위 5개 행을 확인합니다.
display(train_data.head())

# 테스트 데이터의 상위 5개 행을 확인합니다.
display(test_data.head())
훈련용 리뷰 개수: 150000
테스트용 리뷰 개수: 50000
id	document	label
0	9976970	아 더빙.. 진짜 짜증나네요 목소리	0
1	3819312	흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나	1
2	10265843	너무재밓었다그래서보는것을추천한다	0
3	9045019	교도소 이야기구먼 ..솔직히 재미는 없다..평점 조정	0
4	6483659	사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 ...	1


id	document	label
0	6270596	굳 ㅋ	1
1	9274899	GDNTOPCLASSINTHECLUB	0
2	8544678	뭐야 이 평점들은.... 나쁘진 않지만 10점 짜리는 더더욱 아니잖아	0
3	6825595	지루하지는 않은데 완전 막장임... 돈주고 보기에는....	0
4	6723715	3D만 아니었어도 별 다섯 개 줬을텐데.. 왜 3D로 나와서 제 심기를 불편하게 하죠??	0

 

 

 

train_data.dropna(how = 'any') 결측치가있을경우 훈련데이터 행을 제거

train_data.reset_index(drop - True) 인덱스를 0부터 다시 정리.

test_data도 마찬가지로 수행

# document 또는 label에 결측치가 있는 훈련 데이터 행을 제거합니다.
train_data = train_data.dropna(how="any")

# 결측치 제거 후 인덱스를 0부터 다시 정리합니다.
train_data = train_data.reset_index(drop=True)

# document 또는 label에 결측치가 있는 테스트 데이터 행을 제거합니다.
test_data = test_data.dropna(how="any")

# 결측치 제거 후 인덱스를 0부터 다시 정리합니다.
test_data = test_data.reset_index(drop=True)

# 훈련 데이터에 결측치가 남아 있는지 True 또는 False로 확인합니다.
print("훈련 데이터 결측치 존재 여부:", train_data.isnull().values.any())

# 테스트 데이터에 결측치가 남아 있는지 True 또는 False로 확인합니다.
print("테스트 데이터 결측치 존재 여부:", test_data.isnull().values.any())

# 결측치 제거 후 훈련 데이터 개수를 출력합니다.
print("결측치 제거 후 훈련용 리뷰 개수:", len(train_data))

# 결측치 제거 후 테스트 데이터 개수를 출력합니다.
print("결측치 제거 후 테스트용 리뷰 개수:", len(test_data))
훈련 데이터 결측치 존재 여부: False
테스트 데이터 결측치 존재 여부: False
결측치 제거 후 훈련용 리뷰 개수: 149995
결측치 제거 후 테스트용 리뷰 개수: 49997

 

 

 

 

 

klue/bert-base 사용할 한국어 모델 이름

토큰화를 하는 알고리즘을 불러와 사용한다. CLS, SEP, PAD처리.

BertTokenizer.from_pretrained() 저장소에서 불러오기

# 사용할 한국어 BERT 모델 이름을 지정합니다.
MODEL_NAME = "klue/bert-base"

# KLUE BERT 모델에 맞는 토크나이저를 Hugging Face 저장소에서 불러옵니다.
tokenizer = BertTokenizer.from_pretrained(MODEL_NAME)

# 토크나이저가 정상적으로 불러와졌는지 모델 이름을 출력합니다.
print("사용 토크나이저:", MODEL_NAME)

# BERT 문장 시작 토큰과 해당 정수 ID를 출력합니다.
print("CLS 토큰:", tokenizer.cls_token, tokenizer.cls_token_id)

# BERT 문장 종료 토큰과 해당 정수 ID를 출력합니다.
print("SEP 토큰:", tokenizer.sep_token, tokenizer.sep_token_id)

# BERT 패딩 토큰과 해당 정수 ID를 출력합니다.
print("PAD 토큰:", tokenizer.pad_token, tokenizer.pad_token_id)

 

 

 

 

예시문장을 적고. 

tokenizer.tokenize() 토큰단위로 분리.

tokenizer.encode() 정수 id 시퀀스로 변환

# 토큰화와 인코딩을 확인할 예시 문장을 지정합니다.
sample_sentence = "보는내내 그대로 들어맞는 예측 카리스마 없는 악역"

# 예시 문장을 BERT 토큰 단위로 분리합니다.
tokens = tokenizer.tokenize(sample_sentence)

# 예시 문장을 BERT 정수 ID 시퀀스로 변환합니다.
encoded_ids = tokenizer.encode(sample_sentence)

# 토큰화 결과를 출력합니다.
print("토큰화 결과:", tokens)

# 정수 인코딩 결과를 출력합니다.
print("정수 인코딩 결과:", encoded_ids)

# 정수 ID 시퀀스를 다시 문자열로 복원하여 출력합니다.
print("디코딩 결과:", tokenizer.decode(encoded_ids))
토큰화 결과: ['보', '##는', '##내', '##내', '그대로', '들어맞', '##는', '예측', '카리스마', '없', '##는', '악역']
정수 인코딩 결과: [2, 1160, 2259, 2369, 2369, 4311, 20657, 2259, 5501, 13132, 1415, 2259, 23713, 3]
디코딩 결과: [CLS] 보는내내 그대로 들어맞는 예측 카리스마 없는 악역 [SEP]

 

# 정수 ID 하나하나가 어떤 토큰으로 복원되는지 확인합니다.
for token_id in encoded_ids:
    # 현재 정수 ID를 토큰 문자열로 디코딩합니다.
    decoded_token = tokenizer.decode(token_id)

    # 정수 ID와 해당 토큰을 함께 출력합니다.
    print(token_id, "->", decoded_token)
2 -> [CLS]
1160 -> 보
2259 -> ##는
2369 -> ##내
2369 -> ##내
4311 -> 그대로
20657 -> 들어맞
2259 -> ##는
5501 -> 예측
13132 -> 카리스마
1415 -> 없
2259 -> ##는
23713 -> 악역
3 -> [SEP]

 

 

 

두번째 예시문장입력, tokenizer, encode

# WordPiece 분리를 확인할 두 번째 예시 문장을 지정합니다.
oov_sentence = "전율을 일으키는 영화. 다시 보고싶은 영화"

# 두 번째 예시 문장의 토큰화 결과를 출력합니다.
print("토큰화 결과:", tokenizer.tokenize(oov_sentence))

# 두 번째 예시 문장의 정수 인코딩 결과를 출력합니다.
print("정수 인코딩 결과:", tokenizer.encode(oov_sentence))

# 영어 문장도 같은 토크나이저 규칙으로 분리되는지 확인합니다.
for token_id in tokenizer.encode("happy birthday~!"):
    # 영어 예시 문장의 각 정수 ID를 토큰으로 복원합니다.
    print(token_id, "->", tokenizer.decode(token_id))

 

 

 

 

MAX_SEQ_LEN을 128로 설정해서 

tokenizer() 로 128토큰 길이로 맞추고 padding 적용. return_tensors=None으로 파이썬 리스트 형태로 결과를 받는다. 

# BERT에 입력할 문장의 최대 토큰 길이를 128로 설정합니다.
MAX_SEQ_LEN = 128

# 토크나이저가 실제 학습 입력을 만드는 방식을 하나의 예시 문장으로 확인합니다.
encoded_sample = tokenizer(
    oov_sentence,                 # 인코딩할 문장을 입력합니다.
    max_length=MAX_SEQ_LEN,        # 모든 문장을 최대 128 토큰 길이에 맞춥니다.
    padding="max_length",         # 최대 길이보다 짧은 문장은 PAD 토큰으로 채웁니다.
    truncation=True,               # 최대 길이보다 긴 문장은 뒤쪽을 잘라냅니다.
    return_tensors=None            # 파이썬 리스트 형태로 결과를 받습니다.
)

# 단어와 특수 토큰이 정수 ID로 변환된 결과를 출력합니다.
print("input_ids:", encoded_sample["input_ids"])

# 실제 토큰 위치는 1, 패딩 위치는 0으로 표시한 마스크를 출력합니다.
print("attention_mask:", encoded_sample["attention_mask"])

# 한 문장 분류 문제이므로 모든 위치가 0인 세그먼트 ID를 출력합니다.
print("token_type_ids:", encoded_sample["token_type_ids"])

# input_ids 길이가 128인지 확인합니다.
print("input_ids 길이:", len(encoded_sample["input_ids"]))

# 정수 ID를 다시 문자열로 복원하여 확인합니다.
print("복원 문장:", tokenizer.decode(encoded_sample["input_ids"]))
input_ids: [2, 1537, 2534, 2069, 6572, 2259, 3771, 18, 3690, 4530, 2585, 2073, 3771, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
attention_mask: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
token_type_ids: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
input_ids 길이: 128
복원 문장: [CLS] 전율을 일으키는 영화. 다시 보고싶은 영화 [SEP] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD]

 

 

 

 

 

NSMCDataset() 정의

__init__ 초기화

__len, getitem 정의. getitem 시 text를 str() 문자열로 가져와 int()  정수로 가져와 label에 부여하고 self.tokenizer BERT 입력 형식으로 변환해 squeeze를 통해 형태를 [] 로 바꾸어 반환하도록 한다. label은  BCEWidthLogistaLoss에 맞게 float 텐서로 반환한다. 

# PyTorch 학습용 데이터셋 클래스를 정의합니다.
class NSMCDataset(Dataset):
    # 데이터셋 객체가 생성될 때 실행되는 초기화 메서드입니다.
    def __init__(self, texts, labels, tokenizer, max_seq_len):
        # 리뷰 문장 목록을 리스트 형태로 저장합니다.
        self.texts = list(texts)

        # 정답 라벨 목록을 정수 리스트 형태로 저장합니다.
        self.labels = list(labels)

        # BERT 입력 변환에 사용할 토크나이저를 저장합니다.
        self.tokenizer = tokenizer

        # 모든 문장을 맞출 최대 토큰 길이를 저장합니다.
        self.max_seq_len = max_seq_len

    # 데이터셋에 포함된 전체 샘플 개수를 반환합니다.
    def __len__(self):
        # 리뷰 문장 개수를 반환합니다.
        return len(self.texts)

    # 특정 인덱스의 샘플 하나를 반환합니다.
    def __getitem__(self, idx):
        # idx 위치의 리뷰 문장을 문자열로 가져옵니다.
        text = str(self.texts[idx])

        # idx 위치의 라벨을 정수로 가져옵니다.
        label = int(self.labels[idx])

        # 리뷰 문장을 BERT 입력 형식으로 변환합니다.
        encoding = self.tokenizer(
            text,                         # 변환할 리뷰 문장을 입력합니다.
            max_length=self.max_seq_len,   # 최대 길이를 128로 제한합니다.
            padding="max_length",         # 짧은 문장은 PAD 토큰으로 채웁니다.
            truncation=True,               # 긴 문장은 최대 길이에 맞게 자릅니다.
            return_tensors="pt"            # 결과를 PyTorch 텐서 형태로 반환합니다.
        )

        # 모델에 입력할 input_ids, attention_mask, token_type_ids와 라벨을 딕셔너리로 반환합니다.
        return {
            # squeeze(0)는 [1, 128] 형태를 [128] 형태로 바꾸어 DataLoader가 배치 차원을 만들게 합니다.
            "input_ids": encoding["input_ids"].squeeze(0),

            # attention_mask도 [128] 형태로 반환합니다.
            "attention_mask": encoding["attention_mask"].squeeze(0),

            # token_type_ids도 [128] 형태로 반환합니다.
            "token_type_ids": encoding["token_type_ids"].squeeze(0),

            # 이진 분류 라벨은 BCEWithLogitsLoss에 맞게 float 텐서로 반환합니다.
            "label": torch.tensor(label, dtype=torch.float)
        }

 

 

 

 

 

빠른실습에 사용할 훈련 샘플수를 저장해 train_data.sample() 지정한 갯수만큼 가져온다.  train_work, test_work.

훈련데이터를 train_test_split 실제 훈련용과 검증용으로 나눈다. 

train_df.reset_index() 분할된 데이터의 인덱스를 다시 정리 .

# 실습 실행 시간을 줄이기 위해 일부 샘플만 사용할지 결정합니다.
USE_SMALL_SAMPLE = True

# 빠른 실습에 사용할 훈련 샘플 수를 지정합니다.
TRAIN_SAMPLE_SIZE = 3000

# 빠른 실습에 사용할 테스트 샘플 수를 지정합니다.
TEST_SAMPLE_SIZE = 1000

# 일부 샘플만 사용할 경우 데이터프레임에서 앞쪽 일부만 복사합니다.
if USE_SMALL_SAMPLE:
    # 훈련 데이터에서 지정한 개수만큼만 사용합니다.
    train_work = train_data.sample(n=min(TRAIN_SAMPLE_SIZE, len(train_data)), random_state=SEED).reset_index(drop=True)

    # 테스트 데이터에서 지정한 개수만큼만 사용합니다.
    test_work = test_data.sample(n=min(TEST_SAMPLE_SIZE, len(test_data)), random_state=SEED).reset_index(drop=True)
else:
    # 전체 훈련 데이터를 사용합니다.
    train_work = train_data.reset_index(drop=True)

    # 전체 테스트 데이터를 사용합니다.
    test_work = test_data.reset_index(drop=True)

# 훈련 데이터를 실제 훈련용과 검증용으로 나눕니다.
train_df, valid_df = train_test_split(
    train_work,                 # 분할할 훈련 데이터프레임입니다.
    test_size=0.2,              # 전체 중 20%를 검증용으로 사용합니다.
    random_state=SEED,          # 같은 결과가 나오도록 난수를 고정합니다.
    stratify=train_work["label"] # 긍정/부정 비율이 유지되도록 라벨 기준 층화 분할을 합니다.
)

# 분할된 데이터의 인덱스를 다시 정리합니다.
train_df = train_df.reset_index(drop=True)

# 분할된 검증 데이터의 인덱스를 다시 정리합니다.
valid_df = valid_df.reset_index(drop=True)

# 각 데이터 개수를 출력합니다.
print("실제 학습 데이터 개수:", len(train_df))
print("검증 데이터 개수:", len(valid_df))
print("테스트 데이터 개수:", len(test_work))
실제 학습 데이터 개수: 2400
검증 데이터 개수: 600
테스트 데이터 개수: 1000

 

 

 

훈련용 dataset 객체 생성 NSMDataset() 함수를 활용해서 train_dataset, valid_dataset, test_dataset.

batch size크기로 dataloader 생성

# 훈련용 Dataset 객체를 생성합니다.
train_dataset = NSMCDataset(train_df["document"], train_df["label"], tokenizer, MAX_SEQ_LEN)

# 검증용 Dataset 객체를 생성합니다.
valid_dataset = NSMCDataset(valid_df["document"], valid_df["label"], tokenizer, MAX_SEQ_LEN)

# 테스트용 Dataset 객체를 생성합니다.
test_dataset = NSMCDataset(test_work["document"], test_work["label"], tokenizer, MAX_SEQ_LEN)

# 학습용 미니배치 크기를 지정합니다.
BATCH_SIZE = 16

# 훈련용 DataLoader를 생성합니다.
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)

# 검증용 DataLoader를 생성합니다.
valid_loader = DataLoader(valid_dataset, batch_size=BATCH_SIZE, shuffle=False)

# 테스트용 DataLoader를 생성합니다.
test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)

# 첫 번째 훈련 샘플을 가져옵니다.
sample_item = train_dataset[0]

# 첫 번째 샘플의 input_ids를 출력합니다.
print("단어에 대한 정수 인코딩:", sample_item["input_ids"])

# 첫 번째 샘플의 attention_mask를 출력합니다.
print("어텐션 마스크:", sample_item["attention_mask"])

# 첫 번째 샘플의 token_type_ids를 출력합니다.
print("세그먼트 인코딩:", sample_item["token_type_ids"])

# 첫 번째 샘플의 input_ids 길이를 출력합니다.
print("각 인코딩의 길이:", len(sample_item["input_ids"]))

# 첫 번째 샘플의 정수 인코딩을 다시 문장으로 복원하여 출력합니다.
print("정수 인코딩 복원:", tokenizer.decode(sample_item["input_ids"]))

# 첫 번째 샘플의 정답 라벨을 출력합니다.
print("레이블:", sample_item["label"].item())
단어에 대한 정수 인코딩: tensor([    2, 20609,  2154,   772,  2088,  5429,  2179,  3771,  2116,  1039,
         2073,  2147,    16,  1504,  3771,  2259,  4254, 20609,  2052,    27,
         2532,  2772,  2170,  1378,  2496, 31369,  3944,  5825,  2470,  3771,
         2507,  2062,    18,     3,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0])
어텐션 마스크: tensor([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0])
세그먼트 인코딩: tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0])
각 인코딩의 길이: 128
정수 인코딩 복원: [CLS] 평점만 높고 별로인 영화가 많은데, 이 영화는 인터넷 평점이 7점밖에 안되면서 정말 훌륭한 영화였다. [SEP] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD]
레이블: 1.0

 

 

 

 

BERT 기반 이진 감정분류 모델 클래스 정의 BertForBinaryClassification

nn.Module불러오기

init(), BertModel.from_pretrained() KLUE BERT 본체 모델 불러오기

과적합을 줄이기 위해 nn.Dropout(p=0.1) 적용

self.bert.config.hidden_size hidden_size 가져오기 일반적으로 768이다. 해당값을 사용해 linear 만들기 

forward 순전파 계산 정의

pooler_output CLS 토큰을 기반으로 문장 전체 의미를 요약한 벡터 이를 무작위로 비활성화. 

self.classifier() 선형 분류층을 통과시켜  로짓계산.

logits.squeeze(-1), batch_size 1을 batch_size형태로 바꿔 손실 계산을  쉽게한다. 

# BERT 기반 이진 감성 분류 모델 클래스를 정의합니다.
class BertForBinaryClassification(nn.Module):
    # 모델 객체가 생성될 때 실행되는 초기화 메서드입니다.
    def __init__(self, model_name):
        # 부모 클래스인 nn.Module의 초기화 메서드를 실행합니다.
        super().__init__()

        # 사전 학습된 KLUE BERT 본체 모델을 불러옵니다.
        self.bert = BertModel.from_pretrained(model_name)

        # 과적합을 줄이기 위해 분류층 앞에 Dropout을 적용합니다.
        self.dropout = nn.Dropout(p=0.1)

        # BERT의 hidden_size 값을 가져옵니다. klue/bert-base는 일반적으로 768입니다.
        hidden_size = self.bert.config.hidden_size

        # hidden_size 차원의 CLS 표현을 1개의 로짓으로 바꾸는 선형 분류층입니다.
        self.classifier = nn.Linear(hidden_size, 1)

    # 모델의 순전파 계산을 정의합니다.
    def forward(self, input_ids, attention_mask, token_type_ids):
        # BERT 본체에 토큰 ID, 어텐션 마스크, 세그먼트 ID를 입력합니다.
        outputs = self.bert(
            input_ids=input_ids,                 # 문장을 정수 ID로 표현한 입력입니다.
            attention_mask=attention_mask,       # 실제 토큰과 패딩을 구분하는 마스크입니다.
            token_type_ids=token_type_ids        # 한 문장 입력에서는 대부분 0으로 구성됩니다.
        )

        # pooler_output은 CLS 토큰을 기반으로 문장 전체 의미를 요약한 벡터입니다.
        pooled_output = outputs.pooler_output

        # Dropout을 적용하여 일부 뉴런을 무작위로 비활성화합니다.
        dropped_output = self.dropout(pooled_output)

        # 선형 분류층을 통과시켜 긍정 클래스에 대한 로짓을 계산합니다.
        logits = self.classifier(dropped_output)

        # [batch_size, 1] 형태를 [batch_size] 형태로 바꿔 손실 계산을 쉽게 합니다.
        return logits.squeeze(-1)

 

 

 

 

이진 분류 모델 객체 생성 BertForBinaryClassification

# BERT 이진 분류 모델 객체를 생성합니다.
model = BertForBinaryClassification(MODEL_NAME)

# 모델을 CPU 또는 GPU 장치로 이동합니다.
model = model.to(device)

# 모델 구조를 출력하여 BERT와 분류층이 포함되었는지 확인합니다.
print(model)
[transformers] BertModel LOAD REPORT from: klue/bert-base
Key                                        | Status     |  | 
-------------------------------------------+------------+--+-
cls.predictions.transform.LayerNorm.bias   | UNEXPECTED |  | 
cls.predictions.transform.dense.bias       | UNEXPECTED |  | 
cls.seq_relationship.weight                | UNEXPECTED |  | 
cls.seq_relationship.bias                  | UNEXPECTED |  | 
cls.predictions.bias                       | UNEXPECTED |  | 
cls.predictions.transform.dense.weight     | UNEXPECTED |  | 
cls.predictions.transform.LayerNorm.weight | UNEXPECTED |  | 

Notes:
- UNEXPECTED:	can be ignored when loading from different task/architecture; not ok if you expect identical arch.
BertForBinaryClassification(
  (bert): BertModel(
    (embeddings): BertEmbeddings(
      (word_embeddings): Embedding(32000, 768, padding_idx=0)
      (position_embeddings): Embedding(512, 768)
      (token_type_embeddings): Embedding(2, 768)
      (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
      (dropout): Dropout(p=0.1, inplace=False)
    )
    (encoder): BertEncoder(
      (layer): ModuleList(
        (0-11): 12 x BertLayer(
          (attention): BertAttention(
            (self): BertSelfAttention(
              (query): Linear(in_features=768, out_features=768, bias=True)
              (key): Linear(in_features=768, out_features=768, bias=True)
              (value): Linear(in_features=768, out_features=768, bias=True)
              (dropout): Dropout(p=0.1, inplace=False)
            )
            (output): BertSelfOutput(
              (dense): Linear(in_features=768, out_features=768, bias=True)
              (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
              (dropout): Dropout(p=0.1, inplace=False)
            )
          )
...
  )
  (dropout): Dropout(p=0.1, inplace=False)
  (classifier): Linear(in_features=768, out_features=1, bias=True)
)
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...

 

 

 

 

미니배치를 가져와 확인

# DataLoader에서 첫 번째 미니배치를 가져옵니다.
batch = next(iter(train_loader))

# input_ids 텐서를 학습 장치로 이동합니다.
input_ids = batch["input_ids"].to(device)

# attention_mask 텐서를 학습 장치로 이동합니다.
attention_mask = batch["attention_mask"].to(device)

# token_type_ids 텐서를 학습 장치로 이동합니다.
token_type_ids = batch["token_type_ids"].to(device)

# 기울기 계산을 하지 않는 상태에서 출력 형태만 확인합니다.
with torch.no_grad():
    # 모델에 미니배치를 입력하여 로짓을 계산합니다.
    logits = model(input_ids, attention_mask, token_type_ids)

# 로짓 텐서의 형태를 출력합니다.
print("로짓 형태:", logits.shape)

# 로짓 일부 값을 출력합니다.
print("로짓 예시:", logits[:5])

# sigmoid를 적용하여 긍정 확률로 변환한 값을 출력합니다.
print("긍정 확률 예시:", torch.sigmoid(logits[:5]))
로짓 형태: torch.Size([16])
로짓 예시: tensor([-0.5780, -0.5273, -0.2796, -0.9175, -0.4904], device='cuda:0')
긍정 확률 예시: tensor([0.3594, 0.3712, 0.4306, 0.2855, 0.3798], device='cuda:0')

 

 

학습률을 지정해 eoch를 지정해 손실함수생성. 최저기화알고리즘 생성

# 학습률을 지정합니다. BERT 미세 조정에서는 보통 2e-5~5e-5 범위를 자주 사용합니다.
LEARNING_RATE = 5e-5

# 전체 학습 반복 횟수를 지정합니다.
EPOCHS = 2

# 이진 분류에서 로짓 입력을 직접 받는 손실함수를 생성합니다.
criterion = nn.BCEWithLogitsLoss()

# AdamW 최적화 알고리즘을 생성합니다.
optimizer = optim.AdamW(model.parameters(), lr=LEARNING_RATE)

 

 

 

학습함쉬정의

# 한 epoch 동안 모델을 학습하는 함수를 정의합니다.
def train_one_epoch(model, data_loader, criterion, optimizer, device):
    # 모델을 학습 모드로 전환하여 Dropout 등이 활성화되게 합니다.
    model.train()

    # 전체 손실 합계를 저장할 변수를 0으로 초기화합니다.
    total_loss = 0.0

    # 정답을 맞힌 샘플 수를 저장할 변수를 0으로 초기화합니다.
    total_correct = 0

    # 전체 샘플 수를 저장할 변수를 0으로 초기화합니다.
    total_count = 0

    # DataLoader에서 미니배치를 하나씩 꺼내며 반복합니다.
    for batch in tqdm(data_loader, desc="학습 중"):
        # input_ids를 학습 장치로 이동합니다.
        input_ids = batch["input_ids"].to(device)

        # attention_mask를 학습 장치로 이동합니다.
        attention_mask = batch["attention_mask"].to(device)

        # token_type_ids를 학습 장치로 이동합니다.
        token_type_ids = batch["token_type_ids"].to(device)

        # 라벨을 학습 장치로 이동합니다.
        labels = batch["label"].to(device)

        # 이전 미니배치에서 계산된 기울기를 초기화합니다.
        optimizer.zero_grad()

        # 모델에 입력을 넣어 로짓을 계산합니다.
        logits = model(input_ids, attention_mask, token_type_ids)

        # 로짓과 정답 라벨을 비교하여 손실을 계산합니다.
        loss = criterion(logits, labels)

        # 손실을 기준으로 역전파를 수행하여 기울기를 계산합니다.
        loss.backward()

        # 기울기 폭주를 막기 위해 기울기 크기를 1.0 이하로 제한합니다.
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

        # 계산된 기울기를 사용하여 모델 파라미터를 업데이트합니다.
        optimizer.step()

        # 현재 미니배치 손실에 샘플 수를 곱하여 전체 손실 합계에 더합니다.
        total_loss += loss.item() * labels.size(0)

        # 로짓에 sigmoid를 적용한 뒤 0.5 이상이면 긍정으로 예측합니다.
        preds = (torch.sigmoid(logits) >= 0.5).float()

        # 예측값과 정답이 같은 개수를 누적합니다.
        total_correct += (preds == labels).sum().item()

        # 현재 미니배치의 샘플 수를 전체 샘플 수에 더합니다.
        total_count += labels.size(0)

    # 전체 평균 손실을 계산합니다.
    avg_loss = total_loss / total_count

    # 전체 정확도를 계산합니다.
    avg_acc = total_correct / total_count

    # 평균 손실과 정확도를 반환합니다.
    return avg_loss, avg_acc

 

 

검증함수정의

# 검증 또는 테스트 단계에서 모델 성능을 평가하는 함수를 정의합니다.
def evaluate(model, data_loader, criterion, device):
    # 모델을 평가 모드로 전환하여 Dropout 등을 비활성화합니다.
    model.eval()

    # 전체 손실 합계를 저장할 변수를 0으로 초기화합니다.
    total_loss = 0.0

    # 정답을 맞힌 샘플 수를 저장할 변수를 0으로 초기화합니다.
    total_correct = 0

    # 전체 샘플 수를 저장할 변수를 0으로 초기화합니다.
    total_count = 0

    # 평가 과정에서는 기울기를 계산하지 않아 메모리 사용량과 실행 시간을 줄입니다.
    with torch.no_grad():
        # DataLoader에서 미니배치를 하나씩 꺼내며 반복합니다.
        for batch in tqdm(data_loader, desc="평가 중"):
            # input_ids를 학습 장치로 이동합니다.
            input_ids = batch["input_ids"].to(device)

            # attention_mask를 학습 장치로 이동합니다.
            attention_mask = batch["attention_mask"].to(device)

            # token_type_ids를 학습 장치로 이동합니다.
            token_type_ids = batch["token_type_ids"].to(device)

            # 라벨을 학습 장치로 이동합니다.
            labels = batch["label"].to(device)

            # 모델에 입력을 넣어 로짓을 계산합니다.
            logits = model(input_ids, attention_mask, token_type_ids)

            # 로짓과 정답 라벨을 비교하여 손실을 계산합니다.
            loss = criterion(logits, labels)

            # 현재 미니배치 손실에 샘플 수를 곱하여 전체 손실 합계에 더합니다.
            total_loss += loss.item() * labels.size(0)

            # 로짓을 확률로 바꾼 뒤 0.5 이상이면 긍정으로 예측합니다.
            preds = (torch.sigmoid(logits) >= 0.5).float()

            # 예측값과 정답이 같은 개수를 누적합니다.
            total_correct += (preds == labels).sum().item()

            # 현재 미니배치의 샘플 수를 전체 샘플 수에 더합니다.
            total_count += labels.size(0)

    # 전체 평균 손실을 계산합니다.
    avg_loss = total_loss / total_count

    # 전체 정확도를 계산합니다.
    avg_acc = total_correct / total_count

    # 평균 손실과 정확도를 반환합니다.
    return avg_loss, avg_acc

 

 

 

가장 좋은 검증 정확도를 저장할 변수를 0으로 초기화

저장할 파일이름 지정

학습반복. 

평가

이전 최고 정확도보다 높으면 모데 저장 torch.save()

# 가장 좋은 검증 정확도를 저장할 변수를 0으로 초기화합니다.
best_valid_acc = 0.0

# 가장 좋은 모델 가중치를 저장할 파일 이름을 지정합니다.
best_model_path = "best_bert_nsmc_torch.pt"

# 지정한 epoch 수만큼 학습을 반복합니다.
for epoch in range(1, EPOCHS + 1):
    # 현재 epoch 번호를 출력합니다.
    print(f"\n===== Epoch {epoch}/{EPOCHS} =====")

    # 훈련 데이터로 한 epoch 학습을 수행합니다.
    train_loss, train_acc = train_one_epoch(model, train_loader, criterion, optimizer, device)

    # 검증 데이터로 현재 모델 성능을 평가합니다.
    valid_loss, valid_acc = evaluate(model, valid_loader, criterion, device)

    # 현재 epoch의 훈련 손실과 정확도를 출력합니다.
    print(f"훈련 손실: {train_loss:.4f} | 훈련 정확도: {train_acc:.4f}")

    # 현재 epoch의 검증 손실과 정확도를 출력합니다.
    print(f"검증 손실: {valid_loss:.4f} | 검증 정확도: {valid_acc:.4f}")

    # 현재 검증 정확도가 이전 최고 정확도보다 높으면 모델을 저장합니다.
    if valid_acc > best_valid_acc:
        # 최고 검증 정확도를 현재 값으로 갱신합니다.
        best_valid_acc = valid_acc

        # 모델의 학습된 파라미터를 파일로 저장합니다.
        torch.save(model.state_dict(), best_model_path)

        # 모델이 저장되었음을 출력합니다.
        print("최고 검증 정확도 갱신, 모델 저장 완료:", best_model_path)

 

 

 

 

저장된 모델의 가중치 파일이 존재하는지 확인

load_state_dict() evaluate() 평가 .

# 저장된 최고 성능 모델 가중치 파일이 존재하는지 확인합니다.
if os.path.exists(best_model_path):
    # 저장된 최고 성능 모델 가중치를 현재 모델에 불러옵니다.
    model.load_state_dict(torch.load(best_model_path, map_location=device))

    # 가중치 불러오기가 완료되었음을 출력합니다.
    print("저장된 최고 성능 모델을 불러왔습니다.")

# 테스트 데이터로 최종 모델 성능을 평가합니다.
test_loss, test_acc = evaluate(model, test_loader, criterion, device)

# 테스트 손실과 정확도를 출력합니다.
print(f"테스트 손실: {test_loss:.4f} | 테스트 정확도: {test_acc:.4f}")

 

 

 

 

 

긍정 부정감정을 예측하는 함수. 

평가모드로 전환. 

BERT 형식으로 변환. 

학습장치로 이동. 

예측과정에서는 기울기를 계산하지않는다. 

model() 입력문장을 넣어 로짓을 계산. 

sigmoid 를 적용하여 긍정확률로 변환. 

0.5 이상이면 긍정 리뷰로 판단한다. 

# 새로운 문장의 긍정/부정 감성을 예측하는 함수를 정의합니다.
def sentiment_predict(new_sentence):
    # 모델을 평가 모드로 전환합니다.
    model.eval()

    # 입력 문장을 BERT 입력 형식으로 변환합니다.
    encoding = tokenizer(
        new_sentence,              # 예측할 새 리뷰 문장입니다.
        max_length=MAX_SEQ_LEN,     # 학습 때와 같은 최대 길이를 사용합니다.
        padding="max_length",      # 짧은 문장은 PAD 토큰으로 채웁니다.
        truncation=True,            # 긴 문장은 최대 길이에 맞게 자릅니다.
        return_tensors="pt"         # PyTorch 텐서 형태로 반환합니다.
    )

    # input_ids 텐서를 학습 장치로 이동합니다.
    input_ids = encoding["input_ids"].to(device)

    # attention_mask 텐서를 학습 장치로 이동합니다.
    attention_mask = encoding["attention_mask"].to(device)

    # token_type_ids 텐서를 학습 장치로 이동합니다.
    token_type_ids = encoding["token_type_ids"].to(device)

    # 예측 과정에서는 기울기를 계산하지 않습니다.
    with torch.no_grad():
        # 모델에 입력 문장을 넣어 로짓을 계산합니다.
        logit = model(input_ids, attention_mask, token_type_ids)

        # 로짓에 sigmoid를 적용하여 긍정 확률로 변환합니다.
        positive_score = torch.sigmoid(logit).item()

    # 긍정 확률이 0.5 이상이면 긍정 리뷰로 판단합니다.
    if positive_score >= 0.5:
        # 긍정 리뷰 확률을 퍼센트로 출력합니다.
        print(f"{positive_score * 100:.2f}% 확률로 긍정 리뷰입니다.")
    else:
        # 부정 리뷰 확률을 퍼센트로 출력합니다.
        print(f"{(1 - positive_score) * 100:.2f}% 확률로 부정 리뷰입니다.")

    # 다른 코드에서 활용할 수 있도록 긍정 확률을 반환합니다.
    return positive_score

 

 

 

확인

# 부정에 가까운 예시 리뷰를 예측합니다.
sentiment_predict("보던거라 계속 보고 있는데 전개도 느리고 주인공도 너무 소극적으로 나와서 아쉽다")

# 긍정에 가까운 예시 리뷰를 예측합니다.
sentiment_predict("스토리는 조금 아쉬웠지만 배우들의 연기력과 음악이 정말 좋아서 끝까지 몰입해서 봤다")

 

 

 

 

 

 

 

 

 

다른 실습코드를 분석해보자. BERT_Binary_Classification.ipynb

BERT모델을 사용하는 이진분류모델 학습. 

 

 

 

 

transformer 설치

accelerate pytorch 학습장치. 설정을 보조하는 라이브러리로 최신 transformers와 함께 자주사용된다 .

# Colab 환경에서 필요한 패키지를 설치합니다.
# transformers: Hugging Face의 BERT 모델과 Tokenizer를 사용하기 위한 라이브러리입니다.
# accelerate: PyTorch 학습 장치 설정을 보조하는 라이브러리로, 최신 transformers와 함께 자주 사용됩니다.
# scikit-learn: 데이터 분리와 평가 지표 계산에 사용합니다.
!pip -q install transformers accelerate scikit-learn

 

 

 

 

import, seed 고정

transformers BertTokenizerFast 문장을 BERT 입력시 숫자 토큰으로 변환한다 . 위에서 사용해본 BertTokenBase와 비슷함 

transformers BertForSequenceClassification 문장 분류용 출력층이 붙어있는 BERT모델. 출력층을 nn.leanear를 만들어 추가했지만 이번에는 붙어있다. 

transformers get_linear_schedule_with_warmup 학습률을 처음에는 서서히 올리고 이후는 점차 낮추는 스케줄러.  아까는 학습률을 직접 넣었지만 학습률을 관리해주는 스케줄러 .

tqdm 시각적으로 보기위한, 반복문의 진행률

시드고정

# 운영체제 경로 확인, 파일 존재 여부 확인 등에 사용하는 표준 라이브러리입니다.
import os

# 난수 시드를 고정하기 위해 사용하는 표준 라이브러리입니다.
import random

# 수치 계산과 배열 처리를 위해 NumPy를 불러옵니다.
import numpy as np

# 표 형태 데이터를 읽고 전처리하기 위해 Pandas를 불러옵니다.
import pandas as pd

# PyTorch의 핵심 기능인 Tensor, 모델, 학습 연산을 사용하기 위해 torch를 불러옵니다.
import torch

# Dataset은 사용자 정의 데이터셋 클래스를 만들 때 상속받고, DataLoader는 미니배치 단위로 데이터를 공급합니다.
from torch.utils.data import Dataset, DataLoader

# train_test_split은 Train, Validation, Test 데이터를 분리할 때 사용합니다.
from sklearn.model_selection import train_test_split

# classification_report는 분류 모델의 정밀도, 재현율, F1-score를 한 번에 출력합니다.
from sklearn.metrics import accuracy_score, classification_report

# BertTokenizerFast는 문장을 BERT 입력 숫자 토큰으로 변환합니다.
from transformers import BertTokenizerFast

# BertForSequenceClassification은 문장 분류용 출력층이 붙어 있는 BERT 모델입니다.
from transformers import BertForSequenceClassification

# AdamW는 Transformer 계열 모델에서 자주 사용하는 Adam 기반 최적화 함수입니다.
from torch.optim import AdamW

# get_linear_schedule_with_warmup은 학습률을 처음에는 서서히 올리고 이후 점차 낮추는 스케줄러입니다.
from transformers import get_linear_schedule_with_warmup

# tqdm은 반복문의 진행률을 시각적으로 보여줍니다.
from tqdm.auto import tqdm

# 실험 재현성을 위해 사용할 난수 시드 값을 지정합니다.
SEED = 42

# 파이썬 random 모듈의 난수 시드를 고정합니다.
random.seed(SEED)

# NumPy 난수 시드를 고정합니다.
np.random.seed(SEED)

# PyTorch CPU 난수 시드를 고정합니다.
torch.manual_seed(SEED)

# CUDA GPU가 사용 가능하다면 GPU 난수 시드도 고정합니다.
if torch.cuda.is_available():
    torch.cuda.manual_seed_all(SEED)

# GPU가 있으면 cuda를 사용하고, 없으면 cpu를 사용하도록 장치를 설정합니다.
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 현재 사용 중인 학습 장치를 출력합니다.
print("사용 장치:", device)

 

 

 

 

데이터 불러오기

dataframe을 ㅗ읽고 컬럼명을 id, review, sentiment로 지정한다 

# # Google Colab에서 실행 중인지 확인하기 위해 try 문을 사용합니다.
# try:
#     # Colab에서 Google Drive를 연결하기 위한 모듈입니다.
#     from google.colab import drive

#     # Google Drive를 /content/mnt 경로에 마운트합니다.
#     drive.mount("/content/mnt")

# # 로컬 환경이나 Colab이 아닌 환경에서는 google.colab 모듈이 없으므로 예외가 발생할 수 있습니다.
# except Exception as e:
#     # Colab 환경이 아니면 Drive 마운트를 건너뛰고 안내 메시지만 출력합니다.
#     print("Google Drive 마운트를 건너뜁니다:", e)

import google.colab  as gdrive
drive.mount('/content/drive')
DATA_PATH = '/content/drive/MyDrive/data/ratings_train.txt'

# 기본 데이터 파일 경로를 지정합니다. 필요하면 본인 환경에 맞게 수정합니다.
# DATA_PATH = "/content/mnt/MyDrive/data/ratings_train.txt"

# 지정한 경로에 파일이 없을 때 사용할 대체 경로를 지정합니다.
LOCAL_FALLBACK_PATH = "./ratings_train.txt"

# Google Drive 경로에 파일이 있으면 해당 경로를 사용합니다.
if os.path.exists(DATA_PATH):
    data_path = DATA_PATH

# 현재 작업 폴더에 ratings_train.txt가 있으면 대체 경로를 사용합니다.
elif os.path.exists(LOCAL_FALLBACK_PATH):
    data_path = LOCAL_FALLBACK_PATH

# 두 경로 모두 없으면 파일 업로드가 필요하다는 오류를 발생시킵니다.
else:
    raise FileNotFoundError("ratings_train.txt 파일을 Google Drive의 /MyDrive/data/ 또는 현재 폴더에 배치하세요.")

# 탭으로 구분된 영화 리뷰 파일을 Pandas DataFrame으로 읽습니다.
dataset = pd.read_table(data_path)

# PDF 예제와 동일하게 컬럼명을 id, review, sentiment로 지정합니다.
dataset.columns = ["id", "review", "sentiment"]

# 데이터가 정상적으로 읽혔는지 상위 5개 행을 확인합니다.
dataset.head()
id	review	sentiment
0	9976970	더빙이 실망스럽네요..	0
1	3819312	오버연기조차 가볍지 않구나	1
2	10265843	너무재밌었다. 그래서보는것을추천한다	0
3	9045019	솔직히 재미는 없다..평점 조정	0
4	6483659	배우의 익살스런 연기가 돋보였던 영화!	1

 

 

 

데이터갯수 저장

dataset.dropna() reciew, sentiment에 결측치가 있는 행을 reset_index(drop = T) 행 삭제

sentiment 컬럼을 astype(int)형변환

# 결측치 제거 전 데이터 개수를 저장합니다.
before_count = len(dataset)

# review 또는 sentiment에 결측치가 있는 행을 제거합니다.
dataset = dataset.dropna(subset=["review", "sentiment"]).reset_index(drop=True)

# sentiment 컬럼을 정수형으로 변환하여 PyTorch label로 사용할 수 있게 합니다.
dataset["sentiment"] = dataset["sentiment"].astype(int)

# 결측치 제거 후 데이터 개수를 저장합니다.
after_count = len(dataset)

# 제거된 행 개수를 출력합니다.
print("결측치 제거 전 데이터 수:", before_count)

# 남은 행 개수를 출력합니다.
print("결측치 제거 후 데이터 수:", after_count)

# sentiment 값별 데이터 개수를 출력합니다.
print(dataset["sentiment"].value_counts())
결측치 제거 전 데이터 수: 150000
결측치 제거 후 데이터 수: 149995
sentiment
0    75170
1    74825
Name: count, dtype: int64

 

 

 

 

train_test_split() 인덱스와 레이블을 기준으로 인덱스 분리하며 긍정/부정 비율이 유지되도록 stratify wndghk toavmffld wjrdyd

다시 인덱스 분리. 기존 Train중 30%를 Validation으로 사용.

# 전체 데이터의 인덱스와 레이블을 기준으로 Train/Test 인덱스를 분리합니다.
train_idx, test_idx, _, _ = train_test_split(
    dataset.index,                 # 분리할 전체 데이터의 인덱스입니다.
    dataset["sentiment"],          # stratify에 사용할 레이블입니다.
    test_size=0.2,                 # 전체 데이터 중 20%를 Test 데이터로 사용합니다.
    stratify=dataset["sentiment"], # 긍정/부정 비율이 유지되도록 층화 샘플링을 적용합니다.
    random_state=SEED              # 같은 결과가 나오도록 난수 시드를 고정합니다.
)

# Train 인덱스에 해당하는 행을 선택합니다.
train_set = dataset.iloc[train_idx].reset_index(drop=True)

# Test 인덱스에 해당하는 행을 선택합니다.
test_set = dataset.iloc[test_idx].reset_index(drop=True)

# Train 데이터에서 다시 Train/Validation 인덱스를 분리합니다.
train_idx, valid_idx, _, _ = train_test_split(
    train_set.index,                    # 다시 분리할 Train 데이터의 인덱스입니다.
    train_set["sentiment"],             # stratify에 사용할 Train 데이터의 레이블입니다.
    test_size=0.3,                      # 기존 Train 중 30%를 Validation으로 사용합니다.
    stratify=train_set["sentiment"],    # Train/Validation에도 레이블 비율을 유지합니다.
    random_state=SEED                   # 같은 결과가 나오도록 난수 시드를 고정합니다.
)

# Validation 인덱스에 해당하는 행을 선택합니다.
valid_set = train_set.iloc[valid_idx].reset_index(drop=True)

# 최종 Train 인덱스에 해당하는 행을 선택합니다.
train_set = train_set.iloc[train_idx].reset_index(drop=True)

# 각 데이터셋의 크기를 출력합니다.
print("Train:", train_set.shape)

# Validation 데이터셋의 크기를 출력합니다.
print("Validation:", valid_set.shape)

# Test 데이터셋의 크기를 출력합니다.
print("Test:", test_set.shape)
Train: (83997, 3)
Validation: (35999, 3)
Test: (29999, 3)

 

 

 

 

BertMovieReviewDataset 클래스 저으이

init() 각 요소 초기화. 

len정의

getitem시 레이블을 int 정수로 가져와할당, 감성레이블을 정수로 가져와 할당,

self.tokenizer() add_special_tokens T CLS, SEP같은 특수토큰을 자동으로 추가

padding = maxlen 을 기준으로 pad 설정

truncation T maxlen에 맞춰 긴문장 자르기

return_tensors pt pytorch tensor 형태로 결과를 반환한다.

# 영화 리뷰 감성 분류용 Dataset 클래스를 정의합니다.
class BertMovieReviewDataset(Dataset):
    # Dataset 객체가 생성될 때 리뷰, 레이블, Tokenizer, 최대 길이를 저장합니다.
    def __init__(self, reviews, sentiments, tokenizer, max_len=128):
        # 리뷰 문장 리스트를 객체 변수에 저장합니다.
        self.reviews = reviews

        # 정답 레이블 리스트를 객체 변수에 저장합니다.
        self.sentiments = sentiments

        # BERT Tokenizer를 객체 변수에 저장합니다.
        self.tokenizer = tokenizer

        # 모든 문장의 최대 토큰 길이를 저장합니다.
        self.max_len = max_len

    # Dataset의 전체 샘플 개수를 반환합니다.
    def __len__(self):
        # 리뷰 리스트의 길이가 전체 데이터 개수입니다.
        return len(self.reviews)

    # 특정 index에 해당하는 하나의 샘플을 반환합니다.
    def __getitem__(self, index):
        # index 위치의 리뷰를 문자열로 변환하여 가져옵니다.
        review = str(self.reviews[index])

        # index 위치의 감성 레이블을 정수로 가져옵니다.
        sentiment = int(self.sentiments[index])

        # 최신 transformers에서는 encode_plus() 대신 tokenizer(...) 호출 방식을 사용하는 것이 안전합니다.
        # tokenizer(...)는 내부적으로 문장을 토큰화하고, 토큰 ID 변환, 패딩, 자르기, attention_mask 생성을 한 번에 수행합니다.
        encoded = self.tokenizer(
            review,                         # 토큰화할 원본 리뷰 문장입니다.
            add_special_tokens=True,         # [CLS], [SEP] 같은 특수 토큰을 자동으로 추가합니다.
            max_length=self.max_len,         # 문장의 최대 토큰 길이를 지정합니다.
            padding="max_length",           # 짧은 문장은 max_length까지 [PAD] 토큰으로 채웁니다.
            truncation=True,                 # 긴 문장은 max_length에 맞게 자릅니다.
            return_attention_mask=True,      # 실제 토큰과 패딩 토큰을 구분하는 attention_mask를 반환합니다.
            return_token_type_ids=False,     # 단일 문장 분류이므로 token_type_ids는 반환하지 않습니다.
            return_tensors="pt"              # 결과를 PyTorch Tensor 형태로 반환합니다.
        )

        # DataLoader가 사용할 딕셔너리 형태로 하나의 샘플을 반환합니다.
        return {
            "input_ids": encoded["input_ids"].squeeze(0),                # shape을 [1, max_len]에서 [max_len]으로 줄입니다.
            "attention_mask": encoded["attention_mask"].squeeze(0),      # attention_mask도 [max_len] 형태로 만듭니다.
            "labels": torch.tensor(sentiment, dtype=torch.long)           # 분류 정답은 long 타입 Tensor로 변환합니다.
        }

 

 

 

한국어 BERT 모델 kykim/bert-kor-base

BertTokenizerFast.from_pretranined() 다운로드하기

max_len 입력할 최대 토큰 길이 지정

train, vaid, test dataset BertMovieReviewDataset() 객체로 반환

# 사용할 한국어 BERT 모델 이름을 지정합니다.
bert_model_name = "kykim/bert-kor-base"

# Hugging Face Hub에서 사전 학습된 BERT Tokenizer를 다운로드합니다.
tokenizer = BertTokenizerFast.from_pretrained(bert_model_name)

# BERT에 입력할 최대 토큰 길이를 지정합니다.
MAX_LEN = 128

# Train DataFrame을 PyTorch Dataset 객체로 변환합니다.
train_dataset = BertMovieReviewDataset(
    reviews=train_set["review"].tolist(),          # Train 리뷰 문장을 리스트로 전달합니다.
    sentiments=train_set["sentiment"].tolist(),    # Train 정답 레이블을 리스트로 전달합니다.
    tokenizer=tokenizer,                            # 앞에서 다운로드한 Tokenizer를 전달합니다.
    max_len=MAX_LEN                                 # 최대 토큰 길이를 전달합니다.
)

# Validation DataFrame을 PyTorch Dataset 객체로 변환합니다.
valid_dataset = BertMovieReviewDataset(
    reviews=valid_set["review"].tolist(),          # Validation 리뷰 문장을 리스트로 전달합니다.
    sentiments=valid_set["sentiment"].tolist(),    # Validation 정답 레이블을 리스트로 전달합니다.
    tokenizer=tokenizer,                            # 같은 Tokenizer를 사용합니다.
    max_len=MAX_LEN                                 # 같은 최대 토큰 길이를 사용합니다.
)

# Test DataFrame을 PyTorch Dataset 객체로 변환합니다.
test_dataset = BertMovieReviewDataset(
    reviews=test_set["review"].tolist(),           # Test 리뷰 문장을 리스트로 전달합니다.
    sentiments=test_set["sentiment"].tolist(),     # Test 정답 레이블을 리스트로 전달합니다.
    tokenizer=tokenizer,                            # 같은 Tokenizer를 사용합니다.
    max_len=MAX_LEN                                 # 같은 최대 토큰 길이를 사용합니다.
)

# 첫 번째 Train 샘플의 Tensor 구조를 확인합니다.
print(train_dataset[0])

 

 

 

 

dataloader 생성

shuffle 학습시 데이터 순서를 섞어 일반화 성능을 높인다 .

# GPU 메모리에 맞게 Batch Size를 설정합니다. 메모리 부족 시 8 또는 4로 줄입니다.
BATCH_SIZE = 16

# Train Dataset을 미니배치 단위로 섞어서 공급하는 DataLoader를 생성합니다.
train_loader = DataLoader(
    train_dataset,          # 학습용 Dataset입니다.
    batch_size=BATCH_SIZE,  # 한 번에 처리할 샘플 수입니다.
    shuffle=True            # 학습 시 데이터 순서를 섞어 일반화 성능을 높입니다.
)

# Validation Dataset을 미니배치 단위로 공급하는 DataLoader를 생성합니다.
valid_loader = DataLoader(
    valid_dataset,          # 검증용 Dataset입니다.
    batch_size=BATCH_SIZE,  # 검증에서도 같은 Batch Size를 사용합니다.
    shuffle=False           # 검증은 순서를 섞을 필요가 없습니다.
)

# Test Dataset을 미니배치 단위로 공급하는 DataLoader를 생성합니다.
test_loader = DataLoader(
    test_dataset,           # 평가용 Dataset입니다.
    batch_size=BATCH_SIZE,  # 평가용 Batch Size입니다.
    shuffle=False           # 평가도 순서를 섞지 않습니다.
)

 

 

 

라벨 갯수 2개로 설정

BertForSequenceClassification.from_pretrained() 문장분류용 BERT 모델 다운

모델 장치이동

tl_strategy를 3으로 지정. 마지막 encoder와 pooler만 학습한다. 

외 1은 BERT 전체고정, 2는 pooler만 학습한다. 

전략 1은 학습된거 그대로 가져다 쓴다는 의미 . 미세조정을 아예 하지않는다. 

전략 2는 pooler만 미세조정을 한다는 것이다. pooler는 pooler layer의 가중치와 편향으로 문장분류에 직접 사용되는 레이어이다. CLS 벡터를 분류하기 좋은 형태로 변환하는 Poolersms embedding과 encoder과 다르게 분류직전의 레이어로이 작업에서는 어떤 특징을 더 강조해야하는지를 새 데이터에 맞게 다시 배우는게 좋아 선택한다. 

elif tl_strategy == 3 BERT Base계열은 encoder layer가 0~11까지 존재함으로 마지막레이어는 layer.11

학습시키는 레이어를 많이 늘릴수록 새로운 데이터에 더 잘 적용하지만 학습시간도 길어지고 과적합 위험도 커짐으로 데이터 양과 작업에 따라 적절한 전략을 선택해야한다. 

Strategy 1 Classifier만
Strategy 2 Pooler + 마지막 Encoder
Strategy 3 마지막 3개 Encoder + Pooler + Classifier
Strategy 4 BERT 전체

모든 파라미터를 순회해 pooler도 아니고 마지막 encoder layer도 아니면 고정한다. 이는 전이학습할때 매우 자주 사용하는 기법으로 pooler와 마지막 레이어를 지외한 나머지 가중치는 모두 고정한다. 기울기를 계산하지않아 optimizer가 이값을 업데이트하징낳는다. 일부만 학습하는 이유는 이미 충분히 잘 학습된 모델이기 때문이다. BERT는 이미 수억개의 문장으로 미리학습이 되어있어 처음부터 다시 학습시킬 필요가없다. 

# 이진 분류이므로 출력 클래스 개수를 2로 지정합니다.
NUM_LABELS = 2

# 문장 분류용 BERT 모델을 다운로드합니다.
model = BertForSequenceClassification.from_pretrained(
    bert_model_name,      # 사용할 사전 학습 모델 이름입니다.
    num_labels=NUM_LABELS # 출력 클래스 수입니다. 부정/긍정이므로 2입니다.
)

# 모델을 CPU 또는 GPU 장치로 이동합니다.
model = model.to(device)

# Fine-tuning 전략을 지정합니다. 0은 전체 학습, 1은 BERT 전체 고정, 2는 pooler만 학습, 3은 마지막 encoder와 pooler만 학습입니다.
tl_strategy = 3

# 전략 1: BERT 본체 전체를 고정하고 분류기만 학습합니다.
if tl_strategy == 1:
    # BERT 본체의 모든 파라미터를 순회합니다.
    for name, param in model.bert.named_parameters():
        # 현재 파라미터 이름을 확인용으로 출력합니다.
        print(name)

        # 해당 파라미터가 학습되지 않도록 gradient 계산을 끕니다.
        param.requires_grad = False

# 전략 2: pooler를 제외한 대부분의 BERT 본체를 고정합니다.
elif tl_strategy == 2:
    # BERT 본체의 모든 파라미터를 순회합니다.
    for name, param in model.bert.named_parameters():
        # 이름이 pooler로 시작하지 않는 파라미터만 고정합니다.
        if not name.startswith("pooler"):
            # 해당 파라미터가 학습되지 않도록 설정합니다.
            param.requires_grad = False

# 전략 3: 마지막 Encoder Layer와 pooler만 학습합니다.
elif tl_strategy == 3:
    # BERT Base 계열은 보통 encoder layer가 0~11까지 존재하므로 마지막 layer는 layer.11입니다.
    last_layer_name = "layer.11"

    # BERT 본체의 모든 파라미터를 순회합니다.
    for name, param in model.bert.named_parameters():
        # pooler도 아니고 마지막 encoder layer도 아니면 고정합니다.
        if (not name.startswith("pooler")) and (last_layer_name not in name):
            # 해당 파라미터가 학습되지 않도록 gradient 계산을 끕니다.
            param.requires_grad = False

# 학습 가능한 파라미터 수를 계산합니다.
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)

# 전체 파라미터 수를 계산합니다.
total_params = sum(p.numel() for p in model.parameters())

# 학습 가능한 파라미터 비율을 출력합니다.
print(f"학습 가능 파라미터: {trainable_params:,} / 전체 파라미터: {total_params:,}")
[transformers] BertForSequenceClassification LOAD REPORT from: kykim/bert-kor-base
Key                                        | Status     | 
-------------------------------------------+------------+-
cls.predictions.transform.LayerNorm.bias   | UNEXPECTED | 
cls.predictions.decoder.weight             | UNEXPECTED | 
cls.predictions.transform.LayerNorm.weight | UNEXPECTED | 
cls.predictions.transform.dense.weight     | UNEXPECTED | 
cls.seq_relationship.weight                | UNEXPECTED | 
cls.predictions.transform.dense.bias       | UNEXPECTED | 
cls.seq_relationship.bias                  | UNEXPECTED | 
cls.predictions.decoder.bias               | UNEXPECTED | 
cls.predictions.bias                       | UNEXPECTED | 
classifier.weight                          | MISSING    | 
classifier.bias                            | MISSING    | 

Notes:
- UNEXPECTED:	can be ignored when loading from different task/architecture; not ok if you expect identical arch.
- MISSING:	those params were newly initialized because missing from the checkpoint. Consider training on your downstream task.
학습 가능 파라미터: 7,680,002 / 전체 파라미터: 118,298,882

 

 

 

학습하이퍼파라미터 설정 

optimizer 설정

# 전체 학습 반복 횟수를 지정합니다.
EPOCHS = 1

# AdamW Optimizer의 학습률을 지정합니다.
LEARNING_RATE = 2e-5

# Weight Decay는 과적합을 줄이기 위한 정규화 계수입니다.
WEIGHT_DECAY = 0.01

# Warmup Step은 학습 초반 학습률을 천천히 올리는 단계 수입니다.
WARMUP_STEPS = 0

# 학습 가능한 파라미터만 Optimizer에 전달합니다.
optimizer = AdamW(
    filter(lambda p: p.requires_grad, model.parameters()), # requires_grad=True인 파라미터만 업데이트합니다.
    lr=LEARNING_RATE,                                      # 학습률을 설정합니다.
    weight_decay=WEIGHT_DECAY                              # Weight Decay를 설정합니다.
)

# 전체 학습 Step 수를 계산합니다.
total_training_steps = len(train_loader) * EPOCHS

# 선형 학습률 스케줄러를 생성합니다.
scheduler = get_linear_schedule_with_warmup(
    optimizer,                              # 학습률을 조정할 Optimizer입니다.
    num_warmup_steps=WARMUP_STEPS,          # Warmup 단계 수입니다.
    num_training_steps=total_training_steps # 전체 학습 단계 수입니다.
)

 

 

 

 

 

학습 함수와 평가함수 정의

한 epoch를 학습하는 함수를 정의한다. 

torch.nn.utils.clip_grad_norm() 기울기 자르기. 

학습과정은 입력 - 모델 - 예측 - loss계산 - grad 계산 - 가중치 수정으로 진행되는데 가끔 gradient가 엄청 커질 수 있고 이를 gradient explosion 라고 한다. 이 경우 loss가 갑자기 커지고 학습이 불안정해지고 NaN이 발생하기도 해서 잘라준다. norm가 1.보다 크면 전체를 비율대로 줄이라는 의미.

BERT와 같은 대형모델은 파라미터가 매우 많고 미세조정중 grad가 갑자기 커질 수 있어 항상 loss와 optimizer사이에 torch.nn.utils.clip_grad_norm_() 을 사용한다. 

tqdm() 을 사용해 사용자가 진행바를 볼 수 있도록 한다. 

# 한 Epoch 동안 모델을 학습하는 함수를 정의합니다.
def train_one_epoch(model, data_loader, optimizer, scheduler, device):
    # 모델을 학습 모드로 전환하여 Dropout 등이 활성화되게 합니다.
    model.train()

    # Epoch 전체 손실을 누적할 변수를 초기화합니다.
    total_loss = 0.0

    # 실제 정답 레이블을 저장할 리스트를 생성합니다.
    all_labels = []

    # 모델 예측 레이블을 저장할 리스트를 생성합니다.
    all_preds = []

    # DataLoader에서 미니배치를 하나씩 꺼내며 진행률을 표시합니다.
    for batch in tqdm(data_loader, desc="Training"):
        # input_ids Tensor를 학습 장치로 이동합니다.
        input_ids = batch["input_ids"].to(device)

        # attention_mask Tensor를 학습 장치로 이동합니다.
        attention_mask = batch["attention_mask"].to(device)

        # labels Tensor를 학습 장치로 이동합니다.
        labels = batch["labels"].to(device)

        # 이전 Step에서 계산된 gradient를 초기화합니다.
        optimizer.zero_grad()

        # BERT 모델에 입력을 넣어 loss와 logits를 계산합니다.
        outputs = model(
            input_ids=input_ids,             # 토큰 ID 입력입니다.
            attention_mask=attention_mask,   # 패딩 위치를 무시하기 위한 마스크입니다.
            labels=labels                    # 정답 레이블을 넣으면 loss가 자동 계산됩니다.
        )

        # 모델이 계산한 CrossEntropyLoss를 가져옵니다.
        loss = outputs.loss

        # loss를 기준으로 역전파를 수행하여 gradient를 계산합니다.
        loss.backward()

        # gradient 폭주를 방지하기 위해 gradient norm을 제한합니다.
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

        # Optimizer가 파라미터를 업데이트합니다.
        optimizer.step()

        # Scheduler가 학습률을 한 Step 갱신합니다.
        scheduler.step()

        # 현재 배치의 loss 값을 누적합니다.
        total_loss += loss.item()

        # logits에서 가장 큰 값을 가진 클래스 인덱스를 예측값으로 선택합니다.
        preds = torch.argmax(outputs.logits, dim=1)

        # 정답 레이블을 CPU 리스트로 변환하여 누적합니다.
        all_labels.extend(labels.detach().cpu().numpy())

        # 예측 레이블을 CPU 리스트로 변환하여 누적합니다.
        all_preds.extend(preds.detach().cpu().numpy())

    # 평균 loss를 계산합니다.
    avg_loss = total_loss / len(data_loader)

    # 정확도를 계산합니다.
    accuracy = accuracy_score(all_labels, all_preds)

    # 평균 loss와 정확도를 반환합니다.
    return avg_loss, accuracy

# 모델을 평가하는 함수를 정의합니다.
def evaluate(model, data_loader, device):
    # 모델을 평가 모드로 전환하여 Dropout 등을 비활성화합니다.
    model.eval()

    # 평가 전체 손실을 누적할 변수를 초기화합니다.
    total_loss = 0.0

    # 실제 정답 레이블을 저장할 리스트를 생성합니다.
    all_labels = []

    # 모델 예측 레이블을 저장할 리스트를 생성합니다.
    all_preds = []

    # 평가 중에는 gradient 계산이 필요 없으므로 비활성화합니다.
    with torch.no_grad():
        # DataLoader에서 미니배치를 하나씩 꺼냅니다.
        for batch in tqdm(data_loader, desc="Evaluating"):
            # input_ids Tensor를 평가 장치로 이동합니다.
            input_ids = batch["input_ids"].to(device)

            # attention_mask Tensor를 평가 장치로 이동합니다.
            attention_mask = batch["attention_mask"].to(device)

            # labels Tensor를 평가 장치로 이동합니다.
            labels = batch["labels"].to(device)

            # 모델에 입력을 넣어 loss와 logits를 계산합니다.
            outputs = model(
                input_ids=input_ids,           # 토큰 ID 입력입니다.
                attention_mask=attention_mask, # 패딩 위치를 알려주는 마스크입니다.
                labels=labels                  # 정답 레이블입니다.
            )

            # 현재 배치의 loss를 누적합니다.
            total_loss += outputs.loss.item()

            # logits에서 가장 큰 값을 가진 클래스를 예측값으로 선택합니다.
            preds = torch.argmax(outputs.logits, dim=1)

            # 정답 레이블을 CPU 리스트로 변환하여 누적합니다.
            all_labels.extend(labels.detach().cpu().numpy())

            # 예측 레이블을 CPU 리스트로 변환하여 누적합니다.
            all_preds.extend(preds.detach().cpu().numpy())

    # 평균 loss를 계산합니다.
    avg_loss = total_loss / len(data_loader)

    # 정확도를 계산합니다.
    accuracy = accuracy_score(all_labels, all_preds)

    # 평균 loss, 정확도, 전체 정답, 전체 예측을 반환합니다.
    return avg_loss, accuracy, all_labels, all_preds

 

 

 

모델 학습.

# Epoch별 학습을 반복합니다.
for epoch in range(EPOCHS):
    # 현재 Epoch 번호를 출력합니다.
    print(f"\nEpoch {epoch + 1}/{EPOCHS}")

    # Train 데이터로 한 Epoch 학습을 수행합니다.
    train_loss, train_acc = train_one_epoch(model, train_loader, optimizer, scheduler, device)

    # Validation 데이터로 모델 성능을 평가합니다.
    valid_loss, valid_acc, _, _ = evaluate(model, valid_loader, device)

    # Train 손실과 정확도를 출력합니다.
    print(f"Train Loss: {train_loss:.4f} | Train Accuracy: {train_acc:.4f}")

    # Validation 손실과 정확도를 출력합니다.
    print(f"Valid Loss: {valid_loss:.4f} | Valid Accuracy: {valid_acc:.4f}")

 

 

 

 

최종예측 수행, classification_report 확인

# Test 데이터로 최종 평가를 수행합니다.
test_loss, test_acc, test_labels, test_preds = evaluate(model, test_loader, device)

# Test 손실을 출력합니다.
print(f"Test Loss: {test_loss:.4f}")

# Test 정확도를 출력합니다.
print(f"Test Accuracy: {test_acc:.4f}")

# 부정/긍정 클래스별 정밀도, 재현율, F1-score를 출력합니다.
print(classification_report(
    test_labels,                 # 실제 정답 레이블입니다.
    test_preds,                  # 모델 예측 레이블입니다.
    target_names=["부정", "긍정"] # 클래스 이름입니다.
))

 

 

 

 

 

새 문장으로 에측

# 새 리뷰 문장 하나를 입력받아 감성을 예측하는 함수를 정의합니다.
def predict_sentiment(text, model, tokenizer, device, max_len=128):
    # 모델을 평가 모드로 전환합니다.
    model.eval()

    # 입력 문장을 BERT 입력 형식으로 변환합니다.
    encoded = tokenizer(
        text,                          # 예측할 원본 문장입니다.
        add_special_tokens=True,        # [CLS], [SEP] 토큰을 추가합니다.
        max_length=max_len,             # 최대 토큰 길이를 지정합니다.
        padding="max_length",          # 짧은 문장은 패딩합니다.
        truncation=True,                # 긴 문장은 자릅니다.
        return_attention_mask=True,     # attention_mask를 반환합니다.
        return_tensors="pt"             # PyTorch Tensor로 반환합니다.
    )

    # input_ids를 학습 장치로 이동합니다.
    input_ids = encoded["input_ids"].to(device)

    # attention_mask를 학습 장치로 이동합니다.
    attention_mask = encoded["attention_mask"].to(device)

    # 예측 과정에서는 gradient가 필요 없으므로 비활성화합니다.
    with torch.no_grad():
        # 모델에 입력을 넣어 logits를 얻습니다.
        outputs = model(input_ids=input_ids, attention_mask=attention_mask)

        # logits를 softmax에 통과시켜 클래스별 확률로 변환합니다.
        probabilities = torch.softmax(outputs.logits, dim=1)

        # 가장 확률이 높은 클래스 인덱스를 선택합니다.
        predicted_class = torch.argmax(probabilities, dim=1).item()

    # 클래스 인덱스를 사람이 읽기 쉬운 감성명으로 변환합니다.
    label_name = "긍정" if predicted_class == 1 else "부정"

    # 예측 감성명과 클래스별 확률을 반환합니다.
    return label_name, probabilities.squeeze(0).detach().cpu().numpy()

# 예측에 사용할 예시 리뷰 문장을 지정합니다.
example_review = "배우들의 연기가 좋고 스토리도 재미있었습니다."

# 예시 리뷰의 감성을 예측합니다.
label, probs = predict_sentiment(example_review, model, tokenizer, device, MAX_LEN)

# 예측 결과를 출력합니다.
print("입력 문장:", example_review)

# 예측 감성명을 출력합니다.
print("예측 감성:", label)

# 부정/긍정 확률을 출력합니다.
print("[부정 확률, 긍정 확률]:", probs)

 

 

 

 

 

 

 

 

 

 

 

 

다른 실습코드 분석 BERT_Multi_Classification.ipynb BERT 다중 분류

accelerate 학습 장치 설정을 보조하는 라이브러리

# Colab 환경에서 필요한 패키지를 설치합니다.
# transformers: Hugging Face의 BERT 모델과 Tokenizer를 사용하기 위한 라이브러리입니다.
# accelerate: PyTorch 학습 장치 설정을 보조하는 라이브러리로, 최신 transformers와 함께 자주 사용됩니다.
# scikit-learn: 데이터 분리와 평가 지표 계산에 사용합니다.
!pip -q install transformers accelerate scikit-learn

 

 

 

import

transformers BertTokenizerFast 한국어 BERT tokenizer를 불러온다. 

transformers BertForSequenceClassification 다중분류용 BERT 모델을 불러오기 위한 클래스 

trasnformers get_linear_schedule_width_warmup 학습률 스케줄러

시드고정

# 운영체제 경로 처리와 파일 확인에 사용하는 표준 라이브러리입니다.
import os

# 난수 시드 고정을 위해 사용하는 표준 라이브러리입니다.
import random

# 배열 연산과 난수 제어를 위해 NumPy를 불러옵니다.
import numpy as np

# CSV 파일을 읽고 표 형태 데이터를 처리하기 위해 Pandas를 불러옵니다.
import pandas as pd

# PyTorch Tensor, 모델, 학습 연산을 사용하기 위해 torch를 불러옵니다.
import torch

# PyTorch Dataset과 DataLoader를 사용하기 위해 필요한 클래스를 불러옵니다.
from torch.utils.data import Dataset, DataLoader

# 데이터셋을 Train/Validation/Test로 나누기 위해 train_test_split을 불러옵니다.
from sklearn.model_selection import train_test_split

# 정확도와 상세 분류 리포트를 계산하기 위해 평가 함수를 불러옵니다.
from sklearn.metrics import accuracy_score, classification_report

# 한국어 BERT Tokenizer를 불러오기 위한 클래스입니다.
from transformers import BertTokenizerFast

# 문장 다중 분류용 BERT 모델을 불러오기 위한 클래스입니다.
from transformers import BertForSequenceClassification

# Transformer 학습에 적합한 AdamW Optimizer를 불러옵니다.
from torch.optim import AdamW

# 학습률 스케줄러를 불러옵니다.
from transformers import get_linear_schedule_with_warmup

# 반복문 진행률을 표시하기 위해 tqdm을 불러옵니다.
from tqdm.auto import tqdm

# 실험 재현성을 위한 난수 시드를 지정합니다.
SEED = 42

# 파이썬 random 모듈의 난수 시드를 고정합니다.
random.seed(SEED)

# NumPy 난수 시드를 고정합니다.
np.random.seed(SEED)

# PyTorch CPU 난수 시드를 고정합니다.
torch.manual_seed(SEED)

# GPU가 있으면 CUDA 난수 시드도 고정합니다.
if torch.cuda.is_available():
    torch.cuda.manual_seed_all(SEED)

# GPU가 사용 가능하면 cuda, 아니면 cpu를 학습 장치로 설정합니다.
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 현재 학습 장치를 출력합니다.
print("사용 장치:", device)

 

 

 

 

 

 

뉴스데이터 로드

# # Colab에서 직접 파일 업로드를 지원하기 위해 try 문을 사용합니다.
try:
#     # Colab 파일 업로드 기능을 불러옵니다.
    from google.colab import files
    from google.colab import drive
    drive.mount('/content/drive')

# # Colab이 아닌 환경에서는 google.colab 모듈이 없을 수 있습니다.
except Exception:
#     # Colab이 아니면 files 변수를 None으로 둡니다.
    files = None

# # 기본 뉴스 데이터 경로를 지정합니다.
# DATA_PATH = "./data/news.csv"
DATA_PATH = '/content/drive/MyDrive/data/news.csv'

# # 현재 폴더에 news.csv가 있을 때 사용할 대체 경로입니다.
LOCAL_FALLBACK_PATH = "./news.csv"


# ./data/news.csv 파일이 있으면 해당 경로를 사용합니다.
if os.path.exists(DATA_PATH):
    data_path = DATA_PATH

# ./news.csv 파일이 있으면 해당 경로를 사용합니다.
elif os.path.exists(LOCAL_FALLBACK_PATH):
    data_path = LOCAL_FALLBACK_PATH

# 파일이 없고 Colab 업로드 기능을 사용할 수 있으면 업로드를 요청합니다.
elif files is not None:
    # 사용자에게 news.csv 파일 업로드 창을 띄웁니다.
    uploaded = files.upload()

    # 업로드된 첫 번째 파일명을 가져옵니다.
    uploaded_name = next(iter(uploaded.keys()))

    # 업로드된 파일명을 데이터 경로로 사용합니다.
    data_path = uploaded_name

# 모든 방법이 실패하면 파일 없음 오류를 발생시킵니다.
else:
    raise FileNotFoundError("news.csv 파일을 ./data/news.csv 또는 현재 폴더에 배치하세요.")

# 뉴스 CSV 파일을 Pandas DataFrame으로 읽습니다.
dataset = pd.read_csv(data_path)

# 데이터가 정상적으로 읽혔는지 상위 5개 행을 출력합니다.
dataset.head()