이 글은 제가 작업했던 내용을 정리하기 위해 수기 형식으로 작성 된 글입니다.
2022.12.09 - [[신.만.추]] - 신입이 만드는 추천시스템-1(개요)
2022.12.09 - [[신.만.추]] - 신입이 만드는 추천시스템-2(데이터 수집, 스크래핑)
2022.12.09 - [[신.만.추]] - 신입이 만드는 추천시스템-3(셀레니움 최소화)
2022.12.09 - [[신.만.추]] - 신입이 만드는 추천시스템-4(한국어 전처리 및 워드임베딩)
- 텍스트 데이터 수집(크리에이터 컨텐츠, 광고주 제안서)
- 텍스트 데이터 전처리 및 키워드화
- 키워드로 워드임베딩 모델 학습
- 아이템 벡터화
저번에 작성했던 글에서 키워드화 된 데이터를 가지고 모델을 학습시켰고 실제로 유튜브에서 많이 사용되는 단어로 유사도를 확인했을 때 유사도의 유의미한 변화가 있었다. 이번 글에서는 비디오 데이터를 기반으로 해당 크리에이터의 벡터를 만들어 보겠다.
4. 아이템 벡터화
크리에이터는 언제나 다양한 컨텐츠를 제작한다.
시간이 흐름에 따라 기존에 게임위주로 컨텐츠를 올리던 크리에이터가 음악위주로 컨텐츠를 올리기도 한다.
이러한 컨텐츠의 변화는 영상 제목, 영상의 더보기 내용, 태그 등을 통해서 알 수 있다.
따라서 크리에이터의 벡터는 영상 제목, 영상의 더보기 내용, 태그 등을 전처리 한 키워드를 통해 만들어 낼 수 있다.
이전 글에서 영상 제목, 영상의 더보기 내용을 수집하고, 수집한 내용으로 모델을 학습시켜 유의미한 유사도의 증가를 확인했다.
이번글에서는 키워드를 카운팅하여 가중치로 활용하고, 키워드의 벡터를 가중평균내어 크리에이터의 벡터를 만들고 크리에이터 벡터를 기반으로 유사도를 확인한다.
벡터화를 진행하기 이전에 벡터화에 용이하고, 데이터를 비교해 볼 수 있도록 데이터를 추가 수집하려한다.
추가 수집하려하는 데이터는 영상의 태크들이다.
크리에이터의 영상 제목 위쪽의 태그들이나, 더보기 내용에서 해시태그로 되어있는 태그들을 수집하여 벡터화에 추가적으로 사용할 것이다.
데이터 수집에 사용 된 코드는 아래와 같다.
base_url = '<https://www.youtube.com>'
recent_order_para = '/videos'
about_suffix = '/about'
c_url_list = ["<https://www.youtube.com/channel/UCLyq1gz11P-IYFmP093JaAw","https://www.youtube.com/channel/UCPKNKldggioffXPkSmjs5lQ>",'<https://www.youtube.com/channel/UC2NFRq9s2neD_Ml0tPhNC2Q','https://www.youtube.com/channel/UCfpaSruWW3S4dibonKXENjA>']
creator_data = []
for c_url in c_url_list:
res = requests.get(c_url+recent_order_para)
temp = res.text
start_index = temp.find('ytInitialData')
tmp = temp[start_index+16:]
end_index = tmp.find(';')
tmp = tmp[:end_index]
json_dict = json.loads(tmp)
soup = BeautifulSoup(res.content, 'html.parser')
channel_name = soup.find('meta',property="og:title").get('content')
follower = json_dict['header']['c4TabbedHeaderRenderer']['subscriberCountText']['simpleText']
vod_link = []
temp_contents = json_dict['contents']['twoColumnBrowseResultsRenderer']['tabs'][1]['tabRenderer']
if 'content' in temp_contents.keys():
temp_contents = temp_contents['content']['richGridRenderer']['contents']
for cont in temp_contents:
if 'richItemRenderer' not in cont.keys():
continue
else:
temp_link = base_url+cont['richItemRenderer']['content']['videoRenderer']['navigationEndpoint']['commandMetadata']['webCommandMetadata']['url']
vod_link.append(temp_link)
range_target = 10
for url_idx in range(range_target):
url = vod_link[url_idx]
res = requests.get(url)
temp = res.text
soup = BeautifulSoup(res.content, 'html.parser')
start_index = temp.find('ytInitialPlayerResponse')
tmp = temp[start_index+25:]
try:
end_index = tmp.find(';')
tmp = tmp[:end_index]
json_dict = json.loads(tmp)
status = json_dict['playabilityStatus']['status']
more_text = json_dict['microformat']['playerMicroformatRenderer']
except:
end_index = tmp.find(';var')
tmp = tmp[:end_index]
json_dict = json.loads(tmp)
status = json_dict['playabilityStatus']['status']
more_text = json_dict['microformat']['playerMicroformatRenderer']
if 'description' in more_text.keys():
more_text = json_dict['microformat']['playerMicroformatRenderer']['description']['simpleText']
else:
more_text = ''
start_index = temp.find('ytInitialData')
tmp = temp[start_index+16:]
end_index = tmp.find(';')
tmp = tmp[:end_index]
json_dict = json.loads(tmp)
temp_dict = json_dict['contents']['twoColumnWatchNextResults']['results']['results']['contents']
for k in range(len(temp_dict)):
if 'videoPrimaryInfoRenderer' in temp_dict[k].keys():
dict_idx = k
temp_dict = temp_dict[dict_idx]['videoPrimaryInfoRenderer']
vod_title = temp_dict['title']['runs'][0]['text']
view_cnt = soup.find('meta',itemprop = "interactionCount").get('content')
pub_dt = soup.find('meta',itemprop = "datePublished").get('content')
keyword = soup.find('meta',{'name':"keywords"})
if keyword != None:
keyword = keyword.get('content')
keyword = keyword.split(', ')
keyword = ['#'+k for k in keyword]
else:
keyword = ''
top_tag = []
if 'superTitleLink' in temp_dict.keys():
temp_top = temp_dict['superTitleLink']
for t_tag in temp_top['runs']:
if t_tag['text']!=' ':
top_tag.append(t_tag['text'])
if len(top_tag)!=0:
top_tag = ', '.join(top_tag)
keyword = top_tag+', '+', '.join(keyword)
else:
keyword = ', '.join(keyword)
creator_data.append([channel_name, follower, vod_link[i], vod_title, more_text, keyword, view_cnt, pub_dt])
result_df = pd.DataFrame(creator_data,columns=['채널명','구독자수','영상링크','영상제목','더보기내용', '키워드','조회수','업로드날짜'])
아이템 벡터화에 사용할 데이터는 크리에이터 ‘햄찌’, ’쯔양’, ’뷰티숨’, ’우주하마’의 최근영상 10개의 데이터이다.
크리에이터 ‘햄찌’, ’쯔양’은 먹방 크리에이터이고, ’뷰티숨’은 뷰티 크리에이터, ’우주하마’는 게임 크리에이터이다.
해당 크리에이터의 벡터를 만들고, 기존에 학습시켰던 모델로 단순 키워트 평균 벡터로 만든 크리에이터 벡터와 가중 평균 크리에이터 벡터를 비교해 본다.
우선 벡터화부터 진행해보겠다.
벡터화를 위한 키워드 추출
total_keyword = []
w_dict = {}
for i in (range(len(y_list))):
tmp_df = result_df[result_df['채널명']==y_list[i]]
temp = ' '.join(tmp_df.iloc[:]['영상제목'].tolist()[:10])+' '+' '.join(tmp_df.iloc[:]['더보기내용'].tolist()[:10])
total_temp = ' '.join(tmp_df.iloc[:]['영상제목'].tolist()[:10])+' '+' '.join(tmp_df.iloc[:]['더보기내용'].tolist()[:10])
keyword = ' '.join(tmp_df.iloc[:]['키워드'].tolist()[:10])
temp = temp.replace('None','')
temp = re.sub(r"[^가-힣]",' ',temp)
temp = ' '.join(re.split(r"\\s+", temp))
temp = temp.strip()
temp = temp.split(' ')
total_temp = total_temp.replace('None','')
total_temp = re.sub(r"[^가-힣]",' ',total_temp)
total_temp = ' '.join(re.split(r"\\s+", total_temp))
total_temp = total_temp.strip()
total_temp = mecab.nouns(total_temp)
keyword = re.sub(r"[^가-힣0-9]",' ',keyword)
keyword = ' '.join(re.split(r"\\s+", keyword)).strip()
keyword = keyword.split(' ')
temp += total_temp
temp += keyword
temp = [word for word in temp if len(word)>1]
w_dict[i] = Counter(temp)
t_number = len(temp)
temp = list(set(temp))
if len(temp)!=t_number:
temp = [w_v for w_v in w_dict[i].items() if ('댓글') not in w_v[0] and '구독' not in w_v[0] and '좋아요' not in w_v[0] and '채널' not in w_v[0] and '제게' not in w_v[0] and '힘이' not in w_v[0] and '안녕' not in w_v[0] and y_list[i] not in w_v[0]]
temp.sort(key = lambda x : (x[1]), reverse = True)
temp = [w[0] for w in temp if w[1]>2]
total_keyword.append(temp)
<단순 평균 크리에이터 벡터 생성>
y_vec = []
for i in range(len(y_list)):
print(y_list[i])
tmp_keyword = total_keyword[i]
word_vecs = np.zeros(200)
sum_weight = 0
for word in tmp_keyword:
word_vec = trained_model.wv[word]
weight = 1
sum_weight += weight
word_vecs+=word_vec.dot(weight)
# 기업 가중평균 구하기
youtuber_vector = word_vecs/sum_weight
y_vec.append(youtuber_vector)
<가중 평균 크리에이터 벡터 생성>
y_vec_w = []
for i in range(len(y_list)):
print(y_list[i])
tmp_keyword = total_keyword[i]
word_vecs = np.zeros(200)
sum_weight = 0
for word in tmp_keyword:
word_vec = trained_model.wv[word]
weight = w_dict[i][word]
sum_weight += weight
word_vecs+=word_vec.dot(weight)
# 기업 가중평균 구하기
youtuber_vector = word_vecs/sum_weight
y_vec_w.append(youtuber_vector)
가중평균의 경우 w_dict를 활용하는데, w_dict는 위의 키워드를 추출하는 코드에서 확인할 수 있듯이 해당 크리에이터 영상 텍스트 키워드를 카운트하여 사용한다.
벡터화의 결과로 크리에이터의 벡터가 만들어진다.
이제 한번 비교해 보도록 하겠다.
<단순 평균 크리에이터 벡터 위치>
유사도 결과 이미지
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
fontprop = fm.FontProperties(fname='NanumGothic.ttf')
plt.figure(figsize=(14, 10))
pca = PCA(n_components=2)
h_vec = [trained_model.wv[w] for w in total_keyword[0]]
xys = pca.fit_transform(h_vec)
h_xs = xys[:,0]
h_ys = xys[:,1]
plt.scatter(h_xs, h_ys, marker='o', c='yellow',alpha=0.8) # 햄찌
p_vec = [trained_model.wv[w] for w in total_keyword[1]]
xys = pca.fit_transform(p_vec)
p_xs = xys[:,0]
p_ys = xys[:,1]
plt.scatter(p_xs, p_ys, marker='o', c='blue',alpha=0.8) # 쯔양
p_vec = [trained_model.wv[w] for w in total_keyword[2]]
xys = pca.fit_transform(p_vec)
p_xs = xys[:,0]
p_ys = xys[:,1]
plt.scatter(p_xs, p_ys, marker='o', c='green',alpha=0.8) # 우주하마
p_vec = [trained_model.wv[w] for w in total_keyword[3]]
xys = pca.fit_transform(p_vec)
p_xs = xys[:,0]
p_ys = xys[:,1]
plt.scatter(p_xs, p_ys, marker='o', c='black',alpha=0.8) # 뷰티숨
xys = pca.fit_transform(y_vec)
y_xs = xys[:,0]
y_ys = xys[:,1]
plt.scatter(y_xs, y_ys, marker='o', c='red')
plt.annotate(y_list[0], xy = (y_xs[0],y_ys[0]), fontproperties=fontprop)
plt.annotate(y_list[1], xy = (y_xs[1],y_ys[1]), fontproperties=fontprop)
plt.annotate(y_list[2], xy = (y_xs[2],y_ys[2]), fontproperties=fontprop)
plt.annotate(y_list[3], xy = (y_xs[3],y_ys[3]), fontproperties=fontprop)
variable_a = mpatches.Patch(color='yellow',label='HAMZZY')
variable_b = mpatches.Patch(color='blue',label='TZUYANG')
variable_c = mpatches.Patch(color='green',label='WOOJOO')
variable_d = mpatches.Patch(color='black',label='SOOM')
plt.legend(handles=[variable_a, variable_b, variable_c, variable_d],loc='best')
<가중 평균 크리에이터 벡터 위치>
유사도 결과 이미지
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
fontprop = fm.FontProperties(fname='NanumGothic.ttf')
plt.figure(figsize=(14, 10))
pca = PCA(n_components=2)
h_vec = [trained_model.wv[w] for w in total_keyword[0]]
xys = pca.fit_transform(h_vec)
h_xs = xys[:,0]
h_ys = xys[:,1]
plt.scatter(h_xs, h_ys, marker='o', c='yellow',alpha=0.8) # 햄찌
p_vec = [trained_model.wv[w] for w in total_keyword[1]]
xys = pca.fit_transform(p_vec)
p_xs = xys[:,0]
p_ys = xys[:,1]
plt.scatter(p_xs, p_ys, marker='o', c='blue',alpha=0.8) # 쯔양
p_vec = [trained_model.wv[w] for w in total_keyword[2]]
xys = pca.fit_transform(p_vec)
p_xs = xys[:,0]
p_ys = xys[:,1]
plt.scatter(p_xs, p_ys, marker='o', c='green',alpha=0.8) # 우주하마
p_vec = [trained_model.wv[w] for w in total_keyword[3]]
xys = pca.fit_transform(p_vec)
p_xs = xys[:,0]
p_ys = xys[:,1]
plt.scatter(p_xs, p_ys, marker='o', c='black',alpha=0.8) # 뷰티숨
xys = pca.fit_transform(y_vec_w) # 가중치 추가
y_xs = xys[:,0]
y_ys = xys[:,1]
plt.scatter(y_xs, y_ys, marker='o', c='red')
plt.annotate(y_list[0], xy = (y_xs[0],y_ys[0]), fontproperties=fontprop)
plt.annotate(y_list[1], xy = (y_xs[1],y_ys[1]), fontproperties=fontprop)
plt.annotate(y_list[2], xy = (y_xs[2],y_ys[2]), fontproperties=fontprop)
plt.annotate(y_list[3], xy = (y_xs[3],y_ys[3]), fontproperties=fontprop)
variable_a = mpatches.Patch(color='yellow',label='HAMZZY')
variable_b = mpatches.Patch(color='blue',label='TZUYANG')
variable_c = mpatches.Patch(color='green',label='WOOJOO')
variable_d = mpatches.Patch(color='black',label='SOOM')
plt.legend(handles=[variable_a, variable_b, variable_c, variable_d],loc='best')
이렇게 크리에이터의 벡터를 만들어 보았다.
PCA를 활용하여 산점도로 그려봤을 때는 변화가 있긴 하지만, 이 변화가 유사도에 유의미한 변화인지는 확인이 어려워 각 크리에이터 벡터별로 유사도를 확인해 히트맵으로 그려보았다.
<단순 평균 크리에이터 벡터>
<가중 평균 크리에이터 벡터>
예상했던 결과와는 조금 다른 결과가 나오게 되었다. 예상했던 결과는 햄찌-쯔양의 유사도가 좀 더 높게 나오고 반대로 햄찌-우주하마 또는 햄찌-뷰티숨의 유사도는 더 낮게 떨어지는 것이었는데, 햄찌-쯔양의 유사도 결과가 높게 나오지만 단순 평균에 비해서 떨어지는 결과를 만들었다.
이렇게 된다면 유사도가 크게 유의미 하지 않을 수 있다.
실제 추천서비스에서도 유사도가 유의미하지 않다는 피드백을 많이 받았었다.
해당 부분에 대한 내용은 Flask로 웹서버를 구축한 후 결과 리턴 부분에서 추가로 다루도록 하겠다.
일단 해당 데이터로 키워드를 입력했을 때 해당 키워드의 벡터와 유사한 크리에이터를 추천해주는 로직을 구성한 후 Flask로 웹서버를 구축하는 부분으로 넘어가겠다.
'[신.만.추]' 카테고리의 다른 글
신입이 만드는 추천시스템-7(검색성능향상) (0) | 2022.12.10 |
---|---|
신입이 만드는 추천시스템-6(웹서버 구축) (0) | 2022.12.10 |
신입이 만드는 추천시스템-4(한국어 전처리 및 워드임베딩) (0) | 2022.12.09 |
신입이 만드는 추천시스템-3(셀레니움 최소화) (0) | 2022.12.09 |
신입이 만드는 추천시스템-2(데이터 수집, 스크래핑) (2) | 2022.12.09 |