Machine learning/NLP

[NLP] 수능 영어지문을 풀어주는 인공지능 (WMD)

Acdong 2021. 1. 30. 22:43
728x90

이번에 진행한 프로젝트는 

수능 영어 지문 중에서 주제를 찾는 문제를 풀어보는 인공지능을 구현해 보겠습니다.

from nltk import download
from nltk import word_tokenize
from nltk.corpus import stopwords
from nltk.stem.lancaster import LancasterStemmer
from nltk.stem import WordNetLemmatizer
import nltk
download('stopwords')
stop_words = stopwords.words('english')

import pandas as pd
import numpy as np

먼저 기본적인 영어 NLP 를 처리해주는 패키지를 사용했습니다.

def _preprocess(doc):
    doc = doc.lower()  # Lower the text.
    doc = word_tokenize(doc)  # Split into words.
    doc = [w for w in doc if not w in stop_words]  # Remove stopwords.
    doc = [w for w in doc if w.isalpha()]  # Remove numbers and punctuation.
    return doc

def _lemmatize(tags):
    """단어 리스트를 받아 품사를 구분하여 원형으로 바꿔줍니다."""
    result = list()
    n=WordNetLemmatizer()
    for word , tag in tags:
        if tag in ['VB', 'VBD', 'VBG', 'VBN', 'VBP']:
            result.append(n.lemmatize(word,'v'))
        else:
            result.append(n.lemmatize(word))
    return result

그리고 전처리를 해주는 함수와 lemmatize 함수를 선언했습니다.

 

전처리 함수는 문장을 받아 소문자로 변환해주고 토큰화해서 불용어를 제거해 줍니다.

 

lemmatize 함수는 prices 와 같은 단어를 원형 price 로 바꿔주는 패키지로

동사와 동사가 아닌 것들을 구별하여 원형변환을 진행했습니다.

 

Q. 문제

For their own benefit, companies have various ways of offering lower prices. One way of doing this is a
trade discount. It is offered to the shops or businesses that buy goods on a large scale and sell them. There is
also a quantity discount, which is offered to individuals who order quantities of a product. The company gives a
price break to these buyers because they help cut the costs of selling, storing, shipping, and billing. Finally, a
cash discount is a lower price offered to people who pay in cash.

보기

1. "Types of Discount Pricing",
2. "Ways to Improve Products",
3. "How to Buy Products in Cash",
4. "Locations of Discount Stores",
5. "How to Start a Business"

여기서 Word2vec 모델을 사용하는 데 이미 학습이 되어있는 모델 중 최적화된 모델인

 

GoogleNews-vectors-negative300-SLIM 모델을 사용했습니다. ( 검색하시면 다운받으실 수 있습니다.)

import gensim.models.keyedvectors as word2vec
path = './GoogleNews-vectors-negative300-SLIM.bin.gz'
w2vModel = word2vec.KeyedVectors.load_word2vec_format(path, binary=True)

패키지를 임포트 해준 뒤

TextTokens = _preprocess(originText)
textTags = nltk.pos_tag(TextTokens)
text_words = _lemmatize(textTags)

문제의 대한 텍스트를 전처리 해줍니다. 문제 본문 ( originText )

#정답 리스트를 단어별로 쪼개서 원형으로 바꿔줍니다.
resultChoice = list()
for choice in options:
    tempChoice = _lemmatize(nltk.pos_tag(_preprocess(choice)))
    resultChoice.append(tempChoice)

정답도 리스트로 받아 각각의 단어들로 토큰화 해주고 원형으로 바꿔줍니다.

 

# 본문에 있는 단어들과 보기단어들의 wmd의 거리를 계산하여 distanceList에 추가합니다.
# tuple 형식으로 번호를 추가하고 거리가 가까운 순으로 정렬합니다.
distanceList = []
for i in range(len(options)):
    distance = w2vModel.wmdistance(text_words,resultChoice[i])
    distanceList.append((distance , i))

distanceList.sort(key=lambda x : x[0])

여기서 불러온 모델의 wmdistance 를 사용해서 본문과 지문 사이의 거리를 받아옵니다.

 

여기서 WMD 를 간략하게 설명하자면

 

Word2vec 는 단어들간의 임베딩이라면 WMD 는 문장과 문장간의 유사도를 구하는 알고리즘입니다.

문장들 속 단어들의 유클리디안 거리를 구해서 문장간의 유사도를 측정한다고 보면됩니다.

 

 

위 예시 문제는 틀렸네요 . 정답은 1번인데 3번이라고 예측했네요.. ㅠㅠ

 

이렇게 해서 이 모든 과정을 함수로 만듭니다.

def question_solving(originText , options ,answer):
    """
    Type : orginText(str) , options(list) , answer(int)
    0. args 의 전처리 과정을 거칩니다.
    1. 선택보기들과 본문단어들의 거리값들을 모아둔 distanceList에서 가장 가까운거리의 인덱스를 정답이라고 판단합니다.
    2. picked 와 answer를 비교하여 정답이면 1을 리턴 아니면 0을 리턴합니다.
    """
    text_words = _lemmatize(nltk.pos_tag(_preprocess(originText)))
    resultChoice = list()
    for choice in options:
        tempChoice = _lemmatize(nltk.pos_tag(_preprocess(choice)))
        resultChoice.append(tempChoice)
    
    distanceList = list()
    for choiceNum in range(len(options)):
        distance = w2vModel.wmdistance(text_words,resultChoice[choiceNum])
        distanceList.append(distance)
    
    picked = distanceList.index(min(distanceList)) + 1
    
    print("정답은 {}번 입니다.".format(picked))
    if(answer == picked):
        print("정답을 맞췄습니다.")
        return 1
    elif(answer != picked):
        print("틀렸습니다. 실제정답은 {} 입니다".format(answer))
        return 0

실제로 수집한 데이터들을 테스트 해보겠습니다.

data = pd.read_excel('text_data.xlsx')
data = data.replace(np.NaN, 'None')
#question_soving 함수가 정답이면 1을 리턴 하기때문에 맞은갯수 / 문제갯수를 통해 정확도를 계산합니다.
accuracy = 0
columns = ['#1','#2',"#3",'#4','#5','answer']
for i in range(len(data['text'])):
    origin_text = data['text'][i]
    options = data[columns].loc[i].values[:5]
    answer = data[columns].loc[i].values[-1]
    accuracy += question_solving(origin_text,options , answer)
print("정확도 : {}".format((accuracy / len(data['text']))))
print("{} 문제중 {}개의 정답을 맞췄습니다.".format(len(data['text']), accuracy ))

56.25% 의 정확도를 가지고 있네요.

 

56점이면 5~6등급인가요? 수능을 본지 오래되서 모르겠습니다.

 

그럼 한 번 말고 두 번 혹은 세 번만의 문제를 맞춘다면

얼마정도의 정확도를 가지고 있는지 테스트해 보았습니다.

def question_solving_generous(originText , options ,answer):
    """
    question_solving_generous 함수는 몇번시도만에 정답을 맞췄는지를 체크합니다.
    """
    text_words = _lemmatize(nltk.pos_tag(_preprocess(originText)))
    resultChoice = list()
    for choice in options:
        tempChoice = _lemmatize(nltk.pos_tag(_preprocess(choice)))
        resultChoice.append(tempChoice)
    
    distanceList = list()
    for choiceNum in range(len(options)):
        distance = w2vModel.wmdistance(text_words,resultChoice[choiceNum])
        distanceList.append((distance , choiceNum))
    distanceList.sort(key=lambda x : x[0])
    
    picked = distanceList[0][1] + 1
    picked2 = distanceList[1][1] + 1
    picked3 = distanceList[2][1] + 1

    tryOne = 0; tryTwo = 0; tryThree = 0;
    if(answer == picked): tryOne = 1
    elif(answer == picked2): tryTwo = 1
    elif(answer == picked3): tryThree = 1

    return tryOne , tryTwo , tryThree
    
try1 = 0
try2 = 0
try3 = 0
columns = ['#1','#2',"#3",'#4','#5','answer']
for i in range(len(data['text'])):
    origin_text = data['text'][i]
    options = data[columns].loc[i].values[:5]
    answer = data[columns].loc[i].values[-1]
    tryOne , tryTwo , tryThree = question_solving_generous(origin_text,options , answer)
    try1 += tryOne
    try2 += tryOne + tryTwo
    try3 += tryOne + tryTwo + tryThree
    
avg_score = round(try1 / len(data['text']),2) * 100
gen_score = round(try2 / len(data['text']),2) * 100
m_gen_score = round(try3 / len(data['text']),2) * 100
print(f'정답률은 {avg_score}%입니다.')
print(f'너그러운 정답률은 {gen_score}%입니다.' )
print(f'더 너그러운 정답률은 {m_gen_score}%입니다.')

세 번 만의 맞추면 89%의 정확도를 가지고 있네요.

 

수능문제 특성상 문제간의 서로 너무 비슷하지만 아주 쉬운 문제라면 잘 풀 수 있을 것 같네요.

수능 영어 문제 풀이 봇 이였습니다.

 

반응형