<jupyter_start><jupyter_text>2022/01/22/SUN - 형태소 : 단어로서 의미를 가지는 최소 단위- 형태소 분석이란 말뭉치를 이러한 형태소 어근 단위로 쪼개고 각 형태소에 품사 태깅을 부착하는 작업을 일반적으로 지칭한다.<jupyter_code>import warnings
warnings.filterwarnings('ignore')
import pandas as pd
train_df=pd.read_csv('https://raw.githubusercontent.com/e9t/nsmc/master/ratings_train.txt',sep='\t')
train_df.head(3)
train_df['label'].value_counts( )
train_df.info()<jupyter_output><class 'pandas.core.frame.DataFrame'>
RangeIndex: 150000 entries, 0 to 149999
Data columns (total 3 columns):
 #   Column    Non-Null Count   Dtype 
---  ------    --------------   ----- 
 0   id        150000 non-null  int64 
 1   document  149995 non-null  object
 2   label     150000 non-null  int64 
dtypes: int64(2), object(1)
memory usage: 3.4+ MB<jupyter_text>- 1이 긍정, 0이 부정 감성. 균등한 분포를 나타내고 있다.- document 칼럼에 Null이 일부 존재. 이 값은 공백으로 변환. - 문자가 아닌 숫자의 경우 단어적인 의미로 부족하므로 파이썬 정규 표현식 모듈인 re를 이용해 이 역시 공백으로 변환- 테스트 데이터 세트도 로딩후 동일 과정 적용<jupyter_code>import re

train_df = train_df.fillna(' ')
# 정규 표현식을 이용하여 숫자를 공백으로 변경(정규 표현식으로 \d 는 숫자를 의미함.) 
train_df['document'] = train_df['document'].apply( lambda x : re.sub(r"\d+", " ", x) )
train_df.drop('id', axis=1, inplace=True)

# 테스트 데이터 셋을 로딩하고 동일하게 Null 및 숫자를 공백으로 변환
test_df = pd.read_csv('https://raw.githubusercontent.com/e9t/nsmc/master/ratings_test.txt', sep='\t')
test_df = test_df.fillna(' ')
test_df['document'] = test_df['document'].apply( lambda x : re.sub(r"\d+", " ", x) )
test_df.drop('id', axis=1, inplace=True)<jupyter_output><empty_output><jupyter_text>- 이제는 TF-IDF 방식으로 단어를 벡터화하는데 먼저 각 문장을 한글 형태소 분석을 통해 형태소 단어로 토큰화한다. - 한글 형태소 엔진은 SNS분석에 적합한 Twitter 클래스를 이용한다. - Twitter 객체의 morphs() 메서드를 이용하면 입력 인자로 들어온 문장을 형태소 단어 형태로 토큰화해 list 객체로 반환.<jupyter_code>from konlpy.tag import Twitter

twitter = Twitter()
def tw_tokenizer(text):
    # 입력 인자로 들어온 text 를 형태소 단어로 토큰화 하여 list 객체 반환
    tokens_ko = twitter.morphs(text)
    return tokens_ko
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV

# Twitter 객체의 morphs( ) 객체를 이용한 tokenizer를 사용. ngram_range는 (1,2) 
tfidf_vect = TfidfVectorizer(tokenizer=tw_tokenizer, ngram_range=(1,2), min_df=3, max_df=0.9)
tfidf_vect.fit(train_df['document'])
tfidf_matrix_train = tfidf_vect.transform(train_df['document'])<jupyter_output><empty_output><jupyter_text>- 로지스틱 회귀를 이용해 분류 기반의 감성 분석을 수행한다. - 로지스틱 회귀의 하이퍼 파라미터 C의 최적화를 위해 GridSearchCV를 이용한다.  ```python Logistic Regression 을 이용하여 감성 분석 Classification 수행. lg_clf = LogisticRegression(random_state=0) Parameter C 최적화를 위해 GridSearchCV 를 이용. params = { 'C': [1 ,3.5, 4.5, 5.5, 10 ] }grid_cv = GridSearchCV(lg_clf , param_grid=params , cv=3 ,scoring='accuracy', verbose=1 )grid_cv.fit(tfidf_matrix_train , train_df['label'] )print(grid_cv.best_params_ , round(grid_cv.best_score_,4))``` - 이제 테스트 세트를 이용해 최종 감성 분석 예측을 수행한다. - 유의할 것은 재차 언급하지만 테스트 세트를 이용해 예측할 때는 적용한 TfifdVectorizer를 그대로 사용해야 한다. - 그래야만 학습 시 설정된 TfidfVectorizer의 피처 개수와 테스트 데이터를 TfidfVectorizer로 변환할 피처의 개수가 같아진다.<jupyter_code>from sklearn.metrics import accuracy_score

# 학습 데이터를 적용한 TfidfVectorizer를 이용하여 테스트 데이터를 TF-IDF 값으로 Feature 변환함. 
tfidf_matrix_test = tfidf_vect.transform(test_df['document'])

# classifier 는 GridSearchCV에서 최적 파라미터로 학습된 classifier를 그대로 이용
best_estimator = grid_cv.best_estimator_
preds = best_estimator.predict(tfidf_matrix_test)

print('Logistic Regression 정확도: ',accuracy_score(test_df['label'],preds))<jupyter_output>Logistic Regression 정확도:  0.85612<jupyter_text>- 텍스트 분석 실습, 캐글 Mercari Price Suggestion Challenge 파트는 7zip 압축 파일 문제로 업로드하지 않겠음 >  추천 시스템 콘텐츠 기반 필터링(Content based filtering) 방식과 협업 필터링(Collaborative Filtering) 방식으로 나뉜다. 그리고 협업 필터링 방식은 다시 최근접 이웃 협업 필터링과 잠재 요인 협업 필터링으로 나뉜다. - 콘텐츠 기반 필터링 추천 시스템      사용자가 특정한 아이템을 매우 선호하는 경우, 그 아이템과 비슷한 콘텐츠를 가진 다른 아이템을 추천하는 방식이다. 예를 들어 사용자가 특정 영화에 높은 평점을 줬다면 그 영화의 장르, 출연 배우, 감독, 영화 키워드 등의 콘텐츠와 유사한 다른 영화를 추천해주는 방식이다. - 최근접 이웃 협업 필터링      우리가 신작 영화가 나왔을 때 평소 취향이 비슷한 친구에게 그 신작 영화의 평가를 요구하듯 사용자가 아이템에 매긴 평점 정보나 상품 구매 이력과 같은 사용자 행동 양식만을 기반으로 추천을 수행하는 것이 협업 필터링 방식이다. 협업 필터링의 주요 목표는 사용자-아이템 평점 매트릭스와 같은 축적된 사용자 행동 데이터를 기반으로 사용자가 아직 평가하지 않은 아이템을 예측 평가하는 것이다. 협업 필터링은 사용자가 평가한 다른 아이템을 기반으로 사용자가 평가하지 않은 아이템의 예측 평가를 도출하는 방식이다. 최근접 이웃 방식과 잠재 요인 방식 둘 다 사용자-아이템 평점 행렬 데이터에만 의지해 추천을 수행한다. 협업 필터링 알고리즘에 사용되는 사용자-아이템 평점 행렬에서 행은 개별 사용자, 열은 개별 아이템으로 구성되며 사용자 아이디 행, 아이템 아이디 열 위치에 해당하는 값이 평점을 나타내는 형태가 돼야 한다. 판다스의 pivot_table()과 같은 메서드를 통해 이러한 행렬 형태로 적절히 변환할 수도 있겠다.      일반적으로 이러한 사용자-아이템 평점 행렬은 많은 아이템을 열로 가지는 다차원 행렬이며, 사용자가 아이템에 평점을 매기는 경우가 많지 않기 때문에 희소 행렬 특성을 가지고 있다. 최근접 이웃 협업 필터링은 메모리 협업 필터링이라고도 하며 일반적으로 사용자 기반과 아이템 기반으로 다시 나뉠 수 있다.      사용자 기반 : 당신과 비슷한 고객들이 다음 상품도 구매했습니다. 특정 사용자와 유사한 다른 사용자를 TOP-N으로 선정해 이 TOP-N 사용자가 좋아하는 아이템을 추천하는 방식이다. 즉 특정 사용자와 타 사용자 간의 유사도를 측정한 뒤 가장 유사도가 높은 TOP-N 사용자를 추출해 그들이 선호하는 아이템을 추천하는 것이다.      아이템 기반 : 이 상품을 선택한 다른 고객들은 다음 상품도 구매했습니다. 아이템이 가지는 속성과는 상관없이 사용자들이 그 아이템을 좋아하는지/싫어하는지의 평가 척도가 유사한 아이템을 추천하는 기준이 된다. 사용자 기반 최근접 이웃 데이터 세트와 행과 열이 서로 반대이다. 일반적으로 사용자 기반보다는 아이템 기반 협업 필터링이 정확도가 더 높다.- 앞장의 텍스트 분석에서 소개된 유사도 측정 방법인 코사인 유사도는 추천 시스템의 유사도 측정에 가장 많이 적용된다. 추천 시스템에 사용되는 데이터는 피처 벡터화된 텍스트 데이터와 동일하게 다차원 희소 행렬이라는 특징이 있으므로 유사도 측정을 위해 주로 코사인 유사도를 이용한다. - 잠재 요인 협업 필터링의 이해      사용자-아이템 평점 매트릭스 속에 숨어 있는 잠재 요인을 추출해 추천 예측을 할 수 있게 하는 기법이다. 대규모 다차원 행렬을 SVD와 같은 차원 감소 기법으로 분해하는 과정에서 잠재 요인을 추출하는데 이러한 기법을 행렬 분해라고 한다. 사용자-아이템 평점 행렬 데이터만을 이용해 말 그래도 잠재 요인을 끄집어 내는 것을 의미하는데 잠재 요인이 어떤 것인지는 명확이 정의할 수 없다. 하지만 이러한 잠재 요인을 기반으로 다차원 희소 행렬인 사용자-아이템 행렬 데이터를 저차원 밀집 행렬의 사용자-잠재 요인 행렬과 아이템-잠재 요인 행렬의 전치 행렬로 분해할 수 있으며, 이렇게 분해된 두 행렬의 내적을 통해 새로운 예측 사용자-아이템 평점 행렬 데이터를 만들어서 사용자가 아직 평점을 부여하지 않는 아이템에 대한 예측 평점을 생성하는 것이 잠재 요인 협력 필터링 알고리즘의 골자이다.       행렬 분해에 의해 추출되는 잠재 요인이 정확히 어떤 것인지는 알 수 없지만, 가령 영화 평점 기반의 사용자-아이템 평점 행렬 데이터라면 영화가 가지는 장르별 특성 선호도로 가정할 수 있다. 즉 사용자-잠재요인 행렬은 사용자의 영화 장르에 대한 선호도로 아이템 잠재 요인 행렬은 영화의 장르별 특성값으로 정의할 수 있다.       평점이란 사용자의 특정 영화 장르에 대한 선호도와 개별 영화의 그 장르적 특성값을 반영해 결정된다고 생각할 수 있다. 예를 들어 사용자가 액션 영화를 매우 좋아하고 특정 영화가 액션 영화의 특성이 매우 크다면 사용자가 해당 영화에 높은 평점을 줄 것이다. 따라서 평점은 사용자의 장르별 선호도 벡터와 영화의 장르별 특성 벡터를 서로 곱해서 만들 수 있다.       이처럼 잠재 요인 협업 필터링은 숨겨져 있는 잠재 요인을 기반으로 분해된 매트릭스를 이용해 사용자가 아직 평가하지 않은 아이템에 대한 예측 평가를 수행한다. 사용자-아이템 평점 행렬과 같이 다차원의 매트릭스를 저차원의 매트릭스로 분해하는 기법을 행렬 분해라고 한다.  - 행렬 분해      행렬 분해는 다차원의 매트릭스를 저차원 매트릭스로 분해하는 기법으로서 대표적으로 SVD,NMF등이 있다. 인수 분해와 비슷하게 생각하면 된다. M개의 사용자 행과 N개의 아이템 열을 가진 평점 행렬 R은 MXN차원으로 구성되며 행렬 분해를 통해서 사용자-K차원 잠재 요인 행렬 P와 K차원 잠재 요인-아이템 행렬 Q.T로 분해될 수 있다.       사용자가 평가하지 않은 아이템에 대한 평점도 잠재 요인으로 분해된 P행렬과 Q행렬을 이용해 예측할 수 있다.       사용자-아이템 평점 행렬의 NaN 값을 포함한 모든 평점 값은 행렬 분해를 통해 얻어진 P행렬과 Q.T행렬의 내적을 통해 예측 평점으로 다시 계산할 수 있다. ***그렇다면 R행렬을 어떻게 P와 Q행렬로 분해할까. 행렬 분해는 주로 SVD 방식을 이용한다. 하지만 SVD는 NaN값이 없는 행렬에만 적용할 수 있다. NaN값이 포함된 경우에는 확률적 경사 하강법(SGD)이나 ALS방식을 이용해 SVD를 수행한다.*** - 확률적 경사 하강법을 이용한 행렬 분해 : 회귀에서 배운 경사 하강법의 한 종류이다. 확률적 경사 하강법을 이용한 행렬 분석을 요약하자면, P와 Q행렬로 계산된 예측 R 행렬 값이 실제 R 행렬 값과 가장 최소의 오류를 가질 수 있도록 반복적인 비용 함수 최적화를 통해 P와 Q를 유추해내는 것이다. 1. P와 Q를 임의의 값을 가진 행렬로 설정한다.2. P와 Q.T 값을 곱해 예측 R행렬을 계산하고 예측 R행렬과 실제 R행렬에 해당하는 오류 값을 계산한다. 3. 이 오류 값을 최소화할 수 있도록 P와 Q행렬을 적절한 값으로 각각 업데이트한다. 4. 만족할만한 오류 값을 가질 때까지 2,3번 작업을 반복하면서 P와 Q값을 업데이트해 근사화한다. - 일반적으로 사용자-아이템 평점 행렬의 경우 행렬 분해를 위해서 단순히 예측 오류값의 최소화와 학습 시 과적합을 피하기 위해서 규제를 반영한 비용 함수를 적용한다. - L2 규제를 반영해 실제 R 행렬 값과 예측 R 행렬 값의 차이를 최소화하는 방향성을 가지고 P행렬과 Q행렬에 업데이트 값을 반복적으로 수행하면서 최적화된 예측 R 행렬을 구하는 방식이 SGD기반의 행렬분해이다.- SGD를 이용해 행렬 분해를 수행하는 예제를 파이썬으로 구현해보자.- 분해하려는 원본 행렬 R을 P와 Q로 분해한 뒤에 다시 P와 Q.T의 내적으로 예측 행렬을 만드는 예제이다.- 먼저 원본 행렬 R을 미정인 널 값을 포함해 생성하고 분해 행렬 P와 Q는 정규 분포를 가진 랜덤 값으로 초기화한다. 잠재 요인 차원을 3으로 설정한다.<jupyter_code>import numpy as np

# 원본 행렬 R 생성, 분해 행렬 P와 Q 초기화, 잠재요인 차원 K는 3 설정. 
R = np.array([[4, np.NaN, np.NaN, 2, np.NaN ],
              [np.NaN, 5, np.NaN, 3, 1 ],
              [np.NaN, np.NaN, 3, 4, 4 ],
              [5, 2, 1, 2, np.NaN ]])

num_users, num_items = R.shape
K=3

# P와 Q 매트릭스의 크기를 지정하고 정규분포를 가진 random한 값으로 입력합니다. 
np.random.seed(1)
P = np.random.normal(scale=1./K, size=(num_users, K))
Q = np.random.normal(scale=1./K, size=(num_items, K))
print("P:",P)
print("Q:",Q)<jupyter_output>P: [[ 0.54144845 -0.2039188  -0.17605725]
 [-0.35765621  0.28846921 -0.76717957]
 [ 0.58160392 -0.25373563  0.10634637]
 [-0.08312346  0.48736931 -0.68671357]]
Q: [[-0.1074724  -0.12801812  0.37792315]
 [-0.36663042 -0.05747607 -0.29261947]
 [ 0.01407125  0.19427174 -0.36687306]
 [ 0.38157457  0.30053024  0.16749811]
 [ 0.30028532 -0.22790929 -0.04096341]]<jupyter_text>- 다음으로 실제 R 행렬과 예측 행렬의 오차를 구하는 함수를 만들어보자- 이 함수는 실제 R행렬의 널이 아닌 행렬 값의 위치 인덱스를 추출해 이 인덱스에 있는 실제 R행렬 값과 분해된 P,Q를 이용해 다시 조합된 예측 행렬 값의 RMSE 값을 반환한다.<jupyter_code>from sklearn.metrics import mean_squared_error

def get_rmse(R, P, Q, non_zeros):
    error = 0
    # 두개의 분해된 행렬 P와 Q.T의 내적으로 예측 R 행렬 생성
    full_pred_matrix = np.dot(P, Q.T)
    
    # 실제 R 행렬에서 널이 아닌 값의 위치 인덱스 추출하여 실제 R 행렬과 예측 행렬의 RMSE 추출
    x_non_zero_ind = [non_zero[0] for non_zero in non_zeros]
    y_non_zero_ind = [non_zero[1] for non_zero in non_zeros]
    R_non_zeros = R[x_non_zero_ind, y_non_zero_ind]
    full_pred_matrix_non_zeros = full_pred_matrix[x_non_zero_ind, y_non_zero_ind]
      
    mse = mean_squared_error(R_non_zeros, full_pred_matrix_non_zeros)
    rmse = np.sqrt(mse)
    
    return rmse<jupyter_output><empty_output><jupyter_text>- 이제 SGD를 기반으로 행렬 분해를 수행한다. - 먼저 R에서 널 값을 제외한 데이터의 행렬 인덱스를 추출한다. - steps는 SGD를 반복해서 업데이트할 횟수<jupyter_code># R > 0 인 행 위치, 열 위치, 값을 non_zeros 리스트에 저장. 
non_zeros = [ (i, j, R[i,j]) for i in range(num_users) for j in range(num_items) if R[i,j] > 0 ]

steps=1000
learning_rate=0.01
r_lambda=0.01

# SGD 기법으로 P와 Q 매트릭스를 계속 업데이트. 
for step in range(steps):
    for i, j, r in non_zeros:
        # 실제 값과 예측 값의 차이인 오류 값 구함
        eij = r - np.dot(P[i, :], Q[j, :].T)
        # Regularization을 반영한 SGD 업데이트 공식 적용
        P[i,:] = P[i,:] + learning_rate*(eij * Q[j, :] - r_lambda*P[i,:])
        Q[j,:] = Q[j,:] + learning_rate*(eij * P[i, :] - r_lambda*Q[j,:])

    rmse = get_rmse(R, P, Q, non_zeros)
    if (step % 50) == 0 :
        print("### iteration step : ", step," rmse : ", rmse)<jupyter_output>### iteration step :  0  rmse :  3.2388050277987723
### iteration step :  50  rmse :  0.4876723101369647
### iteration step :  100  rmse :  0.15643403848192458
### iteration step :  150  rmse :  0.07455141311978064
### iteration step :  200  rmse :  0.043252267985793146
### iteration step :  250  rmse :  0.029248328780879226
### iteration step :  300  rmse :  0.022621116143829507
### iteration step :  350  rmse :  0.019493636196525232
### iteration step :  400  rmse :  0.018022719092132773
### iteration step :  450  rmse :  0.01731968595344283
### iteration step :  500  rmse :  0.016973657887570985
### iteration step :  550  rmse :  0.01679680459589558
### iteration step :  600  rmse :  0.016701322901884634
### iteration step :  650  rmse :  0.016644736912476574
### iteration step :  700  rmse :  0.016605910068210012
### iteration step :  750  rmse :  0.01657420047570488
### iteration step :  800  rmse :  0.01654431582921612
### iteration step :  850  rmse :  0.016513751774735037
### i[...]<jupyter_text>- 이제 분해된 P와 Q 함수를 P*Q.T로 예측 행렬을 만들어서 출력해보자<jupyter_code>pred_matrix = np.dot(P, Q.T)
print('예측 행렬:\n', np.round(pred_matrix, 3))<jupyter_output>예측 행렬:
 [[3.991 0.897 1.306 2.002 1.663]
 [6.696 4.978 0.979 2.981 1.003]
 [6.677 0.391 2.987 3.977 3.986]
 [4.968 2.005 1.006 2.017 1.14 ]]
{'fraction_non_alphanumeric': 0.09420225517180147, 'fraction_numerical': 0.07404776702477138, 'mean_word_length': 3.2425612052730695, 'pattern_counts': {'":': 0, '<': 29, '<?xml version=': 0, '>': 32, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '8', 'dir': 'jupyter-structured-clean-dedup', 'id': '362708', 'license': 'Apache-2.0', 'n_tokens_mistral': 8342, 'n_tokens_neox': 10121, 'n_words': 1741, 'path': '_notebooks/2022-01-23-intro.ipynb', 'repo_name': 'rhkrehtjd/INTROml', 'size': '11263'}