텍스트 분류하기 - 영어 텍스트 분류(3)

김지원·2022년 11월 22일
0

NLP(자연어 처리)

목록 보기
8/15

💡 영화 리뷰 데이터 분류하기

📌 랜덤 포레스트(Random Forest) 분류 모델

머신러닝 모델 중 하나인 랜덤 포레스트 모델은 여러 개의 의사결정 트리의 결과값을 평균낸 것을 결과로 사용한다.

분류 또는 회귀를 할 수 있다.

🔎의사결정 트리에 대해 먼저 알아보자.
의사결정 트리란 자료구조 중 하나인 트리 구조와 같은 형태로 이뤄진 알고리즘이다. 각 트리에서 구한 결과를 종합해서 결과를 도출하는 것이다.

즉, 많은 트리와 함께 사용함으로써 정확도가 높아진다.

1. CountVectorizer를 활용한 벡터화

모델에 사용할 입력값을 정해야 한다.

선형 회귀모델에서는 TF-IDF와 word2vec을 활용해서 벡터화한 데이터를 입력값으로 사용했다.

CountVectorizer은 동일하게 전처리한 텍스트 데이터를 입력값으로 사용해야 한다.

데이터를 불러온 후, 텍스트 데이터를 특징 벡터로 만들어야 모델에 입력값으로 사용할 수 있다. 이 특징 추출 방법으로 CountVectorizer을 이용해본다.

from sklearn.feature_extraction.text import CountVectorizer

vectorizer = CountVectorizer(analyzer="word", max_features=5000)

train_data_features = vectorizer.fit_transform(reviews)
train_data_features
<25000x5000 sparse matrix of type '<class 'numpy.int64'>'
	with 1975048 stored elements in Compressed Sparse Row format>

결과를 보면 25000개의 데이터가 각각 5000개의 특징값을 갖는 벡터로 표현돼 있다는 것을 볼 수 있다.

2. 모델 구현 및 학습

RandomForestClassifier 객체를 사용해 구현한다.
객체 생성 시, 인자를 설정하는 데 트리의 개수를 의미한다.
아래는 총 100개이 트리를 만들어서 그 결과를 앙상블하고 최종 결과를 만들어낸다.

from sklearn.ensemble import RandomForestClassifier

# 100개의 의사결정 트리를 사용한다.
forest=RandomForestClassifier(n_estimators=100)

# 단어 묶음을 벡터화한 데이터와 정답 데이터를 가지고 학습한다.
forest.fit(train_input, train_label)

3. 검증 데이터셋으로 성능 평가

print("Accuracy: %f"%forest.score(eval_input, eval_label)) # 검증 데이터로 성능 측정
Accuracy: 0.845000

정확도가 약 84%이다.
앙상블 모델인데로 앞서 간단한 모델보다 성능이 좋지 않다.

모델의 문제일 수도 있고 데이터에서 특징을 추출하는 방법의 문제일 수도 있다. (TF-IDF나 word2vec을 사용해 입력값을 만들어 학습하면 ㅓㅅㅇ능이 높아질 수 있다.)

4. 데이터 제출

DATA_IN_PATH = "./"
TEST_CLEAN_DATA='test_clean.csv'
test_data=pd.read_csv(DATA_IN_PATH+TEST_CLEAN_DATA)

test_reviews=list(test_data['review'])
ids = list(test_data['id'])

test_data_features=vectorizer.transform(test_reviews)

result = forest.predict(test_data_features)

output = pd.DataFrame({'id': ids, 'sentiment': result})

output.to_csv(DATA_OUT_PATH+'random_forest_answer.csv', index=False, quoting=3)

📌 순환 신경망(RNN) 분류 모델

이번 모델부터는 딥러닝을 활용해 분류하는 모델을 살펴볼 것이다.

순환 신경망은 언어 모델에서 많이 쓰이는 딥러닝 모델 중 하나다.

주로 순서가 있는 데이터,문장 데이터를 입력해서 문장 흐름에서 패턴을 찾아 분류하게 한다.

단어 특징 벡터를 활용해 모델을 학습하지 않고 ❗텍스트 정보를 입력❗해서 문장에 대한 특징 정보를 추출한다.

순환 신경망(RNN)은 현재 정보는 이전 정보가 점층적으로 쌓이면서 정보를 표현할 수 있는 모델이다. 따라서 순차적인 데이터에 대한 문제에 활용된다.

현재 정보를 "입력 상태(input state)"라 부르고 이전 정보를 "은닉 상태(hidden state)"라 부른다. 이 두 상태 정보를 활용해 순서가 있는 데이터에 대한 예측 모델링을 가능하게 한다.

영화 평점 예측 모델을 만들어보자!

입력 문장을 순차적으로 입력만 하고 마지막으로 입력한 시점에 출력 정보를 뽑아 영화 평점을 예측하고자 한다. 매시간 스텝에 따라 입력되는 input state는 hidden state를 통해 정보를 다음 시간 스텝으로 전달할 수 있게 한다.

🚩마지막 시간 스텝에 나온 hidden state문장 전체 정보가 담긴 정보로서 이 정보를 활용해 영화 평점을 예측하도록 로지스틱 회귀 또는 이진 분류를 하면 된다.

1. 랜덤 시드 고정

모델 구현할 때 가장 중요한 것은 내가 학습한 상황을 그대로 보존하는 것이다. 성능을 재현할 수 있어야 하기 때문이다.

tf.random.set_seed 함수를 이용해 랜덤 시드를 고정하여 학습 하기 위한 랜덤 변수에 대한 초기 상태를 유지할 수 있다.

SEED_NUM = 1234
tf.random.set_seed(SEED_NUM)

2. 학습 데이터 불러오기

DATA_IN_PATH = "./"
TRAIN_INPUT_DATA='train_input.npy'
TRAIN_LABEL_DATA='train_label.npy'
DATA_CONFIGS='data_configs.json'

train_input = np.load(open(DATA_IN_PATH + TRAIN_INPUT_DATA, 'rb'))
train_input = pad_sequences(train_input, maxlen=train_input.shape[1])
train_label = np.load(open(DATA_IN_PATH + TRAIN_LABEL_DATA, 'rb'))
prepro_configs = json.load(open(DATA_IN_PATH + DATA_CONFIGS, 'r'))

텍스트 길이를 맞추기 위해 tensorflow.keras.preprocessing.sequence 모듈에 있는 pad_sequence 함수를 사용했다. 최대 길이를 설정해 모든 데이터에 대한 길이를 최대 길이로 맞추는 역할을 한다.

prepro_configs는 모델에 있는 단어 임베딩 크기를 정의할 때 활용할 것이다.

3. 모델 하이퍼파라미터 정의

model_name = 'rnn_classifier_en'
BATCH_SIZE = 128
NUM_EPOCHS = 5
VALID_SPLIT = 0.1
MAX_LEN = train_input.shape[1]

kargs={'model_name': model_name,
      'vocab_size': prepro_config['vocab_size'],
      'embedding_dimension': 100,
      'dropout_rate':0.2,
      'lstm_dimension':150,
      'dense_dimension':150,
      'output_dimension':1}

각 모델의 레이어 차원 수나 드롭아웃 값을 정하는 하이퍼파라미터는 dict로 정의하여 모델의 __init__ 함수 파라미터에 입력한다.

class RNNClassifier(tf.keras.Model):
    def __init__(self, **kargs):
        super(RNNClassifier, self).__init__(name=kargs['model_name'])
        self.embedding = layers.Embedding(input_dim=kargs['vocab_size'],
                                         output_dim=kargs['embedding_dimension'])
        self.lstm_1_layer=tf.keras.layers.LSTM(kargs['lstm_dimension'],
                                              return_sequences=True)
        self.lstm_2_layer=tf.keras.layers.LSTM(kargs['lstm_dimension'])
        self.dropout = tf.keras.layers.Dropout(kargs['dropout_rate'])
        self.fc1 = tf.keras.layers.Dense(units=kargs['dense_dimension'],
                               activation=tf.keras.activations.tanh)
        self.fc2 = tf.keras.layers.Dense(units=kargs['output_dimension'],
                               activation=tf.keras.activations.sigmoid)
        
    def call(self, x):
        x=self.embedding(x)
        x=self.dropout(x)
        x=self.lstm_1_layer(x)
        x=self.lstm_2_layer(x)
        x=self.dropout(x)
        x=self.fc1(x)
        x=self.dropout(x)
        x=self.fc2(x)
        
        return x

클래스로 모델을 구현하려면 tf.keras.Model을 상속받아야 한다.

class RNNClassifier(tf.keras.Model):

__init__ 함수 구현을 보면 먼저 super 함수가 있다.
super 함수를 통해 부모 클래스에 있는 __init__ 함수를 호출한다.

텍스트 워드 임베딩 벡터를 위해 layers.Embedding 객체를 생성하고, 입력 파라미터로 데이터 사전 수단어 임베딩 차원 수를 입력한다.

RNN Classification 모델은 워드 임베딩 👉 RNN 레이어 👉 RNN 레이어 시퀀스의 마지막 은닉 상태 벡터를 사용한다.

RNN 계열 모델 중 하나인 LSTM을 2개의 레이어로 활용한다.

tf.keras.layers.LSTM 객체를 생성하고, 입력 파라미터로 레이어 출력 차원 수출력 시퀀스를 전부 출력할지 여부를 보는 return_sequences를 입력한다. True로 지정할 경우 시퀀스 형태의 은닉 상태 벡터가 출력될 것이다.

첫 레이어에서 시퀀스 은닉 상태 벡터를 출력해서 다음 레이어에 입력할 시퀀스 벡터를 구성하고(True 지정), 마지막 레이어에서는 시퀀스의 마지막 스텝의 은닉 상태 벡터를 출력해야 할 것이다.

       ...
       self.lstm_1_layer=tf.keras.layers.LSTM(kargs['lstm_dimension'],
                                              return_sequences=True)
        self.lstm_2_layer=tf.keras.layers.LSTM(kargs['lstm_dimension'])
        ...

이제 RNN 레이어에서 출력한 상태 벡터가 피드 포워드 네트워크를 거치게 한다. tf.keras.layers.Dense를 통해 객체 생성, 피드 포워드 네트워크를 구성한다. 입력 파라미터로 units : 네트워크를 출력할 때 나오는 벡터 차원 수와 tanh: 네트워크에서 사용할 활성화 함수를 지정한다.

피드 포워드 네트워크를 거쳐 나온 벡터를 가지고 회귀(regression)할 수 있도록 만들어야 한다. Dense가 위의 네트워크를 거쳐 나온 상태 벡터에서 회귀하게 할 수 있다. tf.keras.layers.Dense를 통해 객체 생성, units=1, activation에는 tf.keras.activations.sigmoid를 지정한다. 이렇게 구현하면 Dense 레이어를 통해 예측한 값을 0~1 값으로 표현할 수 있게 된다.

마지막으로 Droptout 선언한다. 과적합을 방지하기 위한 레이어다.
파라미터 값은 Dropout을 적용할 비율값이다.

4. 모델 생성

model=RNNClassifier(**kargs)
model.compile(optimizer=tf.keras.optimizers.Adam(1e-4),
             loss=tf.keras.losses.BinaryCrossentropy(),
             metrics=[tf.keras.metrics.BinaryAccuracy(name='accuracy')])

5. 모델 학습

from keras.callbacks import EarlyStopping, ModelCheckpoint

earlystop_callback = EarlyStopping(monitor='val_accuracy', min_delta=0.0001, patience=2)
checkpoint_path = DATA_OUT_PATH + model_name + '/weights.h5'
checkpoint_dir = os.path.dirname(checkpoint_path)

if os.path.exists(checkpoint_dir):
    print("{} - Foler already exists \n".format(checkpoint_dir))
else:
    os.makedirs(checkpoint_dir, exist_ok=True)
    print("{} - Foler create complete \n".format(checkpoint_dir))
    
cp_callback = ModelCheckpoint(
checkpoint_path, monitor = 'val_accuracy', verbose=1, save_best_only=True, save_weights_only=True)

model.fit 함수만 사용해도 모델 학습이 진행되지만, 모델 학습 중 발생하는 문제를 해결해야 할 필요가 있다.

오버피팅 현상(학습 평가 점수는 높아지는데 검증 평가 점수가 낮아지는 현상) 혹은 학습 도중 특정 상태의 모델에서 하이퍼파라미터를 바꿔서 다시 학습을 진행할 수도 있다.

이를 위해 tensorflow.keras.callback 모듈에 있는 EarlyStoppingModelCheckpoint 클래스를 활용할 수 있다.

🔎EarlyStopping : 오버피팅 현상을 방지하기 위해 특정 epoch에서 현재 검증 평가 점수가 이전 검증 평가 점수보다 일정 수치 미만으로 낮아지면 학습을 멈추는 역할을 한다.

  • val_accuracy는 검증 평가 점수로 활용한다.
  • 위에서는 0.0001로 설정했으므로 해당 값보다 낮아지면 오버피팅 현상이라고 판단하고 학습을 멈춘다.
  • patience 파라미터는 검증 평가 점수가 이전 최고 점수보다 높아지지 않는 epoch 수가 이 값보다 커지만 학습을 멈춘다.

🔎ModelCheckpoint: epoch마다 모델을 저장하게 한다.

  • save_best_only : 가장 성능이 좋은 모델만 저장한다.
    - 기준은 monitor 파라미터에서 평가지표를 정하면 되고, 여기서는 val_accuracy로 설정한다.
  • save_weights_only: 모델 그래프를 전부 저장하는 것이 아닌 모델 가중치만 저장하는 옵션이다.
history = model.fit(train_input, train_label, batch_size=BATCH_SIZE, epochs=NUM_EPOCHS, validation_split=VALID_SPLIT, callbacks=[earlystop_callback, cp_callback])

위처럼 model.fit 함수를 실행하여 학습을 시작한다.

Epoch 1/5
176/176 [==============================] - 223s 1s/step - loss: 0.6931 - accuracy: 0.5053 - val_loss: 0.6929 - val_accuracy: 0.5096

Epoch 00001: val_accuracy improved from -inf to 0.50960, saving model to ./rnn_classifier_en/weights.h5
Epoch 2/5
176/176 [==============================] - 190s 1s/step - loss: 0.6805 - accuracy: 0.5345 - val_loss: 0.4860 - val_accuracy: 0.7576

Epoch 00002: val_accuracy improved from 0.50960 to 0.75760, saving model to ./rnn_classifier_en/weights.h5
Epoch 3/5
176/176 [==============================] - 194s 1s/step - loss: 0.3408 - accuracy: 0.8566 - val_loss: 0.2773 - val_accuracy: 0.8844

Epoch 00003: val_accuracy improved from 0.75760 to 0.88440, saving model to ./rnn_classifier_en/weights.h5
Epoch 4/5
176/176 [==============================] - 187s 1s/step - loss: 0.1876 - accuracy: 0.9303 - val_loss: 0.2783 - val_accuracy: 0.8888

Epoch 00004: val_accuracy improved from 0.88440 to 0.88880, saving model to ./rnn_classifier_en/weights.h5
Epoch 5/5
176/176 [==============================] - 183s 1s/step - loss: 0.1291 - accuracy: 0.9576 - val_loss: 0.2997 - val_accuracy: 0.8872

Epoch 00005: val_accuracy did not improve from 0.88880

6. 모델 성능 확인

def plot_graphs(history, string):
    plt.plot(history.history[string])
    plt.plot(history.history['val_'+string],'')
    plt.xlabel("Epochs")
    plt.ylabel(string)
    plt.legend([string, 'val_'+string])
    plt.show()
# 손실값 그래프 
plot_graphs(history,'loss')

# 정확도 그래프
plot_graphs(history, 'accuracy')

7. 데이터 제출

profile
Make your lives Extraordinary!

0개의 댓글