def groupby(from_dtm, to_dtm, tmp_dir, out_path, num_chunks=10): from_dtm, to_dtm = map(str, [from_dtm, to_dtm]) fouts = { idx: open(os.path.join(tmp_dir, str(idx)), 'w') for idx in range(num_chunks) } files = sorted([path for path, _ in iterate_data_files(from_dtm, to_dtm)]) for path in tqdm.tqdm(files, mininterval=1): for line in open(path): user = line.strip().split()[0] chunk_index = mmh3.hash(user, 17) % num_chunks fouts[chunk_index].write(line) map(lambda x: x.close(), fouts.values()) with open(out_path, 'w') as fout: for chunk_idx in fouts.keys(): _groupby = {} chunk_path = os.path.join(tmp_dir, str(chunk_idx)) for line in open(chunk_path): tkns = line.strip().split() userid, seen = tkns[0], tkns[1:] _groupby.setdefault(userid, []).extend(seen) os.remove(chunk_path) for userid, seen in six.iteritems(_groupby): fout.write('%s %s\n' % (userid, ' '.join(seen)))
def _get_seens(self, users): """_get_seens는 사용자들이 본 @유저의 목록을 구성하는 함수입니다. Args: users : 브런치 (사용자해시값)의 list 입니다. Return: seens : {(사용자해시값) : [@이 사용자가 본 유저의 목록]}로 구성된 dict입니다. """ #사용자 목록의 중복을 제거합니다. set_users = set(users) seens = {} #from_dtm과 to_dtm 사이의 있는 파일의 경로를 받아 반복합니다. for path, _ in tqdm.tqdm(iterate_data_files(self.from_dtm, self.to_dtm), mininterval=1): #경로에 있는 파일을 열어서 모든 line을 차례대로 읽습니다. #line은 (사용자의 해시값) (@사용자가 열람한 유저들) 로 이뤄진 하나의 문자열입니다. for line in open(path): #(사용자의 해시값)과 (@사용자가 열람한 유저들)로 구성된 리스트를 만듭니다. tkns = line.strip().split() #userid 는 (사용자의 해시값)입니다. #seen은 (@사용자가 열람한 유저들)로 구성된 리스트입니다. userid, seen = tkns[0], tkns[1:] #유저 아이디가 사용자 목록에 없다면 그냥 넘어갑니다. if userid not in set_users: continue #유저 아이디가 사용자 목록에 있다면 #seens는 userid를 키로 갖고 seen을 값으로 갖도록 합니다. #seens = {userid : [seen]} seens[userid] = seen return seens
def read_reads(): files = sorted( [path for path, _ in iterate_data_files('2018100100', '2019030100')]) for path in tqdm.tqdm(files, mininterval=1): date = path[11:19] for line in open(path): tokens = line.strip().split() user_id = tokens[0] reads = tokens[1:] if user_id in t_users: if user_id in t_reads: t_reads[user_id] += reads else: t_reads[user_id] = reads if date >= "20190222": if user_id in t_reads_dup: t_reads_dup[user_id] += reads else: t_reads_dup[user_id] = reads else: if user_id in reads: o_reads[user_id] += reads else: o_reads[user_id] = reads if date >= "20190222": if user_id in reads_dup: reads_dup[user_id] += reads else: reads_dup[user_id] = reads
def groupby(from_dtm, to_dtm, tmp_dir, out_path, num_chunks=10): # 2018100100 2019022200 ./tmp/ ./tmp/train from_dtm, to_dtm = map( str, [from_dtm, to_dtm ]) # function, 리스트를 받아 리스트 인자들을 함수에 넣은 결과를 다시 리스트로 반환한다. fouts = { idx: open(os.path.join(tmp_dir, str(idx)), 'w') for idx in range(num_chunks) } # 딕셔너리 형태로 파일의 객체를 저장, 경로는 tmp_dir\idx files = sorted([path for path, _ in iterate_data_files(from_dtm, to_dtm) ]) # _ 언더 스코어로 두번째 인자값 무시하고 path만 쓴다, 오름차순대로 정렬된 리스트를 갖는다 # print(files) 리스트 첫번째 인자 : './res/read\\2018100100_2018100101', 리스트 마지막 인자 : './res/read\\2019022123_2019022200' # 유저마다 10개의 파일중 하나의 파일에 들어가게 된다 유저아이디 아티클 for path in tqdm.tqdm( files, desc="files to path", mininterval=1 ): # tqdm으로 for문의 상태를 알 수 있다 실행시 상태바가 뜬다! 1초마다 상태바가 갱신된다 for line in open(path): # path의 파일을 열고 한줄마다 실행해준다 user = line.strip().split()[0] # 줄의 첫번째 단어가 유저 chunk_index = mmh3.hash( user, 17) % num_chunks # 유저를 해쉬화하고 10으로 나눈값에 따라 다른 임시파일에 정보가 들어가게 된다 fouts[chunk_index].write( line) # 미리 만들어둔 딕셔너리로 chunk_index에 해당하는 키값의 파일에 해당 라인을 쓴다 map(lambda x: x.close(), fouts.values( )) # 딕셔너리의 value만 갖고 있는 dict_values객체를 받아 즉 파일들을 받고 map으로 인자를 접근해 전부 닫아준다 # 파일마다 유저아이디에 따라 본 아티클을 통합해준다, 중복된 유저아이디가 없이 파일마다 유저 아이디 이후 본 아티클이 정리된다 with open( out_path, 'w' ) as fout: # 파일을 열고 사용한 후에 닫는 것을 보장하는 문법, try: finally: 구조나 file.__enter__(), __exit__()를 따로 구현한 코드를 안써도 된다. 반환값을 fout이 받는다 for chunk_idx in fouts.keys(): # 키값에 대해서 모두 반복한다 _groupby = { } # 언더스코어는 private한 변수임을 의미 import했을때 접근하지 못하는정도의 private수준 chunk_path = os.path.join(tmp_dir, str(chunk_idx)) # 키값을 붙인 새로운 path를 저장 for line in open(chunk_path): # 임시 path의 한 라인마다 실행해준다 tkns = line.strip().split() # 토큰별로 나누어준 리스트를 반환 userid, seen = tkns[0], tkns[ 1:] # 토큰의 첫번째는 유저아이디이고 그 이후부터는 본 아티클을 얘기한다 _groupby.setdefault(userid, []).extend( seen ) # userid를 키값으로 설정하구 value는 []를 디폴트로 한다. 해당 키값 value에 seen의 리스트 내용을 뒤에 추가해준다 for userid, seen in six.iteritems( _groupby): # 딕셔너리의 키값마다 반복하고 키값과 value값을 반환한다 fout.write( '%s %s\n' % (userid, ' '.join(seen)) ) # 최종 out_path경로 파일에 유저아이디와 본 아티클을 스페이스로 구분하여서 한줄마다 쓴다 os.remove(os.path.join( tmp_dir, str(chunk_idx))) # 해당 경로의 파일을 삭제, 이유 모를 에러 발생 직접 삭제해주자...
def _get_seens(self, users): set_users = set(users) seens = {} for path, _ in tqdm.tqdm(iterate_data_files(self.from_dtm, self.to_dtm), mininterval=1): for line in open(path): tkns = line.strip().split() userid, seen = tkns[0], tkns[1:] if userid not in set_users: continue seens[userid] = seen return seens
def groupby(from_dtm, to_dtm, tmp_dir, out_path, num_chunks=10): """groupby는 일정한 시간간격의 파일들을 합쳐 하나의 그룹 파일로 만드는 함수입니다. Args: from_dtm : 시작날짜입니다. YYYYMMDDHH to_dtm : 종료날짜입니다. YYYYMMDDHH tmp_dir : 주요 파일이 저장되는 tmp 파일의 경로입니다. out_path : 출력될 파일의 경로입니다. num_chunks : 그룹화 되기전 만들어질 청크의 개수입니다. """ #문자열을 받는, from_dtm부터 to_dtm까지의 맵 객체를 만들어 from_dtm과 to_dtm에 저장합니다. from_dtm, to_dtm = map(str, [from_dtm, to_dtm]) #fouts은 {num_chucks : 쓰기로 열려있는 (tmp_dir/idx) }로 구성된 dict입니다. fouts = { idx: open(os.path.join(tmp_dir, str(idx)), 'w') for idx in range(num_chunks) } #files는 from_dtm과 to_dtm 사이에 있는 파일들의 리스트입니다, #시간순으로 정렬되어 있습니다. files = sorted([path for path, _ in iterate_data_files(from_dtm, to_dtm)]) #path는 files에서 원소를 가져옵니다. for path in tqdm.tqdm(files, mininterval=1): #path의 한 라인을 읽습니다. #path의 한 라인은 다음과 같이 구성되어 있는 문자열 입니다. #(사용자의 해시값) (@사용자가 열람한 유저들) for line in open(path): #user은 (사용자의 해시값)을 가져옵니다. user = line.strip().split()[0] #user에 대해 hash값을 생성한 다음 num_chucks로 범위를 줄입니다. chunk_index = mmh3.hash(user, 17) % num_chunks #chunk_index 파일에 line을 씁니다. fouts[chunk_index].write(line) #fout으로 열려있는 파일을 모두 닫습니다. list(map(lambda x: x.close(), fouts.values())) #0~num_chunks 까지 만들어 놓은 임시 파일을 out_path로 grouping 하는 과정 with open(out_path, 'w') as fout: for chunk_idx in fouts.keys(): _groupby = {} chunk_path = os.path.join(tmp_dir, str(chunk_idx)) for line in open(chunk_path): tkns = line.strip().split() userid, seen = tkns[0], tkns[1:] _groupby.setdefault(userid, []).extend(seen) os.remove(chunk_path) for userid, seen in six.iteritems(_groupby): fout.write('%s %s\n' % (userid, ' '.join(seen))) fout.close()
def make_sentence(): session_sentences = [] prev_hour_reads = {} of1 = open("session_sentences_1h.txt", "w") of2 = open("session_sentences_2h.txt", "w") print("read reads for the test set") num_noread = 0 files = sorted([path for path, _ in iterate_data_files('2018100100', '2019030100')]) for path in tqdm.tqdm(files, mininterval=1): datehour = path[11:21] hour_reads = {} for line in open(path): tokens = line.strip().split() user_id = tokens[0] reads = tokens[1:] if len(reads) == 0: num_noread += 1 continue ureads = [] # remove continuously doubled article prev_read = None for read in reads: if prev_read == None or read != prev_read: ureads.append(read) prev_read = read hour_reads[user_id] = ureads if len(ureads) > 1: of1.write(" ".join(ureads) + "\n") curr_hour_reads = hour_reads.copy() for user_id in hour_reads: if user_id in prev_hour_reads: if len(prev_hour_reads[user_id]) + len(hour_reads[user_id]) > 1: of2.write(" ".join(prev_hour_reads[user_id]) + " " + " ".join(hour_reads[user_id]) + "\n") del prev_hour_reads[user_id] del curr_hour_reads[user_id] for user_id in prev_hour_reads: if len(prev_hour_reads[user_id]) > 1: of2.write(" ".join(prev_hour_reads[user_id]) + "\n") prev_hour_reads = curr_hour_reads.copy() of1.close() of2.close() print("no read:", num_noread)
def _build_model(self): model_path = self._get_model_path() if os.path.isfile(model_path): return freq = {} print('building model..') for path, _ in tqdm.tqdm(iterate_data_files(self.from_dtm, self.to_dtm), mininterval=1): for line in open(path): seen = line.strip().split()[1:] for s in seen: freq[s] = freq.get(s, 0) + 1 freq = sorted(freq.items(), key=lambda x: x[1], reverse=True) open(model_path, 'wb').write(cPickle.dumps(freq, 2)) print('model built')
def read_files_to_dataframe(dest_path): print('read files of all users') data = [] files = sorted( [path for path, _ in iterate_data_files('2018100100', '2019022200')]) for path in tqdm(files, mininterval=1): read_datetime = path.split('/')[-1].split('_')[0][:9] for line in open(path): tokens = line.strip().split() user_id = tokens[0] reads = tokens[1:] for item in reads: data.append([read_datetime, user_id, item]) read_df = pd.DataFrame(data) read_df.columns = ['date', 'user_id', 'article_id'] read_df.to_csv(os.path.join(dest_path, 'read_df.csv'), index=False, encoding='utf-8-sig')
def _build_model(self): model_path = self._get_model_path() if os.path.isfile(model_path): return freq = {} print("building model..") for path, _ in tqdm.tqdm( iterate_data_files(self.from_dtm, self.to_dtm), mininterval=1 ): for line in open(path): seen = line.strip().split()[1:] # 어떤 유저가 조회한 글 목록 for s in seen: freq[s] = freq.get(s, 0) + 1 # 등장한 글 하나씩 빈도 추가 freq = sorted( freq.items(), key=lambda x: x[1], reverse=True ) # [(글, 빈도), ...] 형태의 빈도 리스트 open(model_path, "wb").write(cPickle.dumps(freq, 2)) print("model built")
def read_reads(): print("read reads for the test set") files = sorted( [path for path, _ in iterate_data_files('2018100100', '2019030100')]) for path in tqdm.tqdm(files, mininterval=1): date = path[11:19] for line in open(path): tokens = line.strip().split() user_id = tokens[0] reads = tokens[1:] if len(reads) < 1: continue if user_id in t_users: if user_id in t_reads: t_reads[user_id] += reads else: t_reads[user_id] = reads if date >= "20190222": if user_id in t_reads_dup: t_reads_dup[user_id] += reads else: t_reads_dup[user_id] = reads reads_set = set(reads) for read in reads_set: writer = read.split("_")[0] if user_id not in t_followings or writer not in t_followings[ user_id]: if user_id in t_non_follows: if writer in t_non_follows[user_id]: t_non_follows[user_id][writer] += 1 else: t_non_follows[user_id][writer] = 1 else: t_non_follows[user_id] = {} t_non_follows[user_id][writer] = 1 for user in t_reads: if user not in t_reads_dup: t_reads_dup[user] = t_reads[user][-10:]
def _build_model(self): model_path = self._get_model_path() if os.path.isfile(model_path): # 파일이 존재하면 끝내고 없으면 모델이 있는 파일을 만들어 준다 return freq = {} print('building model...') # 기간내에 아티클마다 조회수가 저장되게 된다 for path, _ in tqdm.tqdm(iterate_data_files(self.from_dtm, self.to_dtm), mininterval=1): for line in open(path): seen = line.strip().split()[1:] for s in seen: freq[s] = freq.get( s, 0 ) + 1 # 해당 키값 s에 해당하는 value값에 1을 더한걸 value값으로 갱신 value값이 없는경우 0으로 초기화해서 1더한걸 리턴 freq = sorted(freq.items(), key=lambda x: x[1], reverse=True) # value값에 따른 내림차순으로 정렬 # print(freq) # ('@hotelscombined_399', 1) 와 같은 인자를 가진 리스트로 저장 open(model_path, 'wb').write(cPickle.dumps(freq, 2)) print('model built')
def _build_popular_model(self): model_path = self._get_model_path() if os.path.isfile(model_path): return freq = {} print('building model..') for path, _ in tqdm.tqdm( iterate_data_files(self.from_dtm, self.to_dtm), # 이 기간 사이에 많이 읽힌 글을 추천 mininterval=1): for line in open(path): seen = line.strip().split()[ 1:] # read에서 읽어온 한 줄 한줄을 쪼개고 뒷부분이 읽은글리스트 for s in seen: # 줄마다 읽은글리스트의 읽은글들에 대해 freq[s] = freq.get( s, 0 ) + 1 # freq이라는 dictionary의 읽은글의 frequency값을 가져오고 없으면 0을 가져옴(default 0),그리고 1더해줌 freq = sorted(freq.items(), key=lambda x: x[1], reverse=True) # freq을 정렬할 때 reverse로.. open(model_path, 'wb').write(cPickle.dumps(freq, 2)) print('model built')
def _build_model(self): """_build_model은 사용자에게 유저를 추천하는 모델을 만드는 함수입니다. 사용자에게 추천되는 기준은 빈번하게 열람당하는 유저입니다. """ #model_path은 str 클래스 입니다. 모델이 위치한 경로를 갖습니다. model_path = self._get_model_path() #만약 모델이 있다면 build 과정을 멈춘다. if os.path.isfile(model_path): return #freq는 딕셔너리입니다. #"사용자가 열람한 유저"를 키로 갖습니다. 그 유저의 등장횟수를 값으로 갖습니다. freq = {} print('building model..') #iterate_data_files의 반환값은 from_dtm과 to_dtm 사이에 있는 (파일의 경로와, 파일 이름)입니다. #이 반복문은 파일의 경로만 받고, 파일의 이름은 _로 무시합니다. for path, _ in tqdm.tqdm(iterate_data_files(self.from_dtm, self.to_dtm), mininterval=1): #경로에 있는 파일을 열어서 모든 line을 차례대로 읽습니다. #line은 (사용자의 해시값) (@사용자가 열람한 유저들) 로 이뤄진 하나의 문자열입니다. for line in open(path): #seen은 (@사용자가 열람한 유저들)로 이뤄진 list입니다. seen = line.strip().split()[1:] for s in seen: #s는 seen list에 나타나는 한 (@유저) 입니다. #s는 freq에서 '키'로 사용됩니다. #freq.get(s, 0) + 1을 통해 s가 등장할때 마다 빈도수가 늘어납니다. freq[s] = freq.get(s, 0) + 1 #freq.items()는 ['유저', 빈도 ] 로 구성된 리스트입니다. #lambda x: x[1]은 freq.items()[1]을 조회합니다. 이는 곧 빈도를 조회합니다. #reverse가 True이기 때문에 내림차순으로 정렬합니다. 이는 첫 원소가 빈도수가 높은 유저임을 의미합니다. freq = sorted(freq.items(), key=lambda x: x[1], reverse=True) #모델의 경로에 freq내용을 직렬화 하여 저장합니다. open(model_path, 'wb').write(cPickle.dumps(freq, 2)) print('model built')
def read_reads(): print("read reads of all users") files = sorted( [path for path, _ in iterate_data_files('2018100100', '2019030100')]) for path in tqdm.tqdm(files, mininterval=1): date = path[11:19] for line in open(path): tokens = line.strip().split() user_id = tokens[0] reads = tokens[1:] if len(reads) < 1: continue if user_id in t_users: # 예측 대상 사용자들 if user_id in t_reads: # 예측 대상 사용자들의 조회인 경우 t_reads에 저장 t_reads[user_id] += reads # 동일한 사용자가 서로 다른 시간에 접속하여 조회한 경우 조회한 글을 t_reads의 하나의 키에 시간의 순서를 유지하면서 모아서 저장 else: t_reads[user_id] = reads # 테스트 데이터랑 중첩된 날짜에 대해선 t_reads_dup에 저장 if date >= "20190222": if user_id in t_reads_dup: t_reads_dup[user_id] += reads else: t_reads_dup[user_id] = reads reads_set = set(reads) # 같은 세션 내에서 여러번 조회한 경우 중복을 제거하기 위함 for read in reads_set: writer = read.split("_")[0] # t_followings: 각 유저의 구독 작가 목록 딕셔너리 if (user_id not in t_followings) or ( writer not in t_followings[user_id]): if user_id in t_non_follows: if writer in t_non_follows[ user_id]: # 구독 관계가 없는 작가에 대해 읽은 횟수를 저장 (키=독자아이디/작가아이디, 값=읽은 횟수) t_non_follows[user_id][writer] += 1 else: t_non_follows[user_id][writer] = 1 else: t_non_follows[user_id] = {} t_non_follows[user_id][writer] = 1 num_reads_n1 = len(reads) - 1 # 바로 다음 글 조회 기록 저장 for i, read in enumerate(reads): if i < num_reads_n1: if read == reads[i + 1]: continue # when two continous reads are the same (바로 다음글이 같은 글이면 continue) if read in seq_reads: if reads[i + 1] in seq_reads[read]: seq_reads[read][reads[ i + 1]] += 1 # seq_reads: 연속 조회 통계 저장 (바로 다음 조회한 글의 아이디를 저장!!) else: seq_reads[read][reads[i + 1]] = 1 else: seq_reads[read] = {} seq_reads[read][reads[i + 1]] = 1 # 바로 이전 글 조회 기록 저장 for i, read in enumerate(reads): if i < num_reads_n1: nread = reads[i + 1] if read == nread: continue # when two continous reads are the same if nread in prev_reads: if read in prev_reads[nread]: prev_reads[nread][read] += 1 else: prev_reads[nread][read] = 1 else: prev_reads[nread] = {} prev_reads[nread][read] = 1 # 예측 대상 사용자들이 중첩 기간에 조회한 글이 하나도 없는 경우, 가장 최근에 읽은 글 10개를 t_reads_dup에 저장 for user in t_reads: if user not in t_reads_dup: t_reads_dup[user] = t_reads[user][ -10:] # t_reads_dup: 예측 대상 사용자들의 글 조회
def read_reads(): print("read reads of all users") files = sorted( [path for path, _ in iterate_data_files('2018100100', '2019030100')]) for path in tqdm.tqdm(files, mininterval=1): date = path[11:19] for line in open(path): tokens = line.strip().split() user_id = tokens[0] reads = tokens[1:] if len(reads) < 1: continue if user_id in t_users: if user_id in t_reads: t_reads[user_id] += reads else: t_reads[user_id] = reads if date >= "20190222": if user_id in t_reads_dup: t_reads_dup[user_id] += reads else: t_reads_dup[user_id] = reads reads_set = set(reads) for read in reads_set: writer = read.split("_")[0] if (user_id not in t_followings) or ( writer not in t_followings[user_id]): if user_id in t_non_follows: if writer in t_non_follows[user_id]: t_non_follows[user_id][writer] += 1 else: t_non_follows[user_id][writer] = 1 else: t_non_follows[user_id] = {} t_non_follows[user_id][writer] = 1 num_reads_n1 = len(reads) - 1 for i, read in enumerate(reads): if i < num_reads_n1: if read == reads[i + 1]: continue # when two continous reads are the same if read in seq_reads: if reads[i + 1] in seq_reads[read]: seq_reads[read][reads[i + 1]] += 1 else: seq_reads[read][reads[i + 1]] = 1 else: seq_reads[read] = {} seq_reads[read][reads[i + 1]] = 1 for i, read in enumerate(reads): if i < num_reads_n1: nread = reads[i + 1] if read == nread: continue # when two continous reads are the same if nread in prev_reads: if read in prev_reads[nread]: prev_reads[nread][read] += 1 else: prev_reads[nread][read] = 1 else: prev_reads[nread] = {} prev_reads[nread][read] = 1 for user in t_reads: if user not in t_reads_dup: t_reads_dup[user] = t_reads[user][-10:]
def main(): print("Start inference!") test_end_date = "20190314" """ 아래 파라미터는 실험에 의해 적합한 값을 고름 test_days_len: 테스트 날짜로부터 이후 날짜 수 ex) 20 additional_days_len: 테스트 날짜로부터 이전 날짜 수 ex) 4 테스트 시작 날짜로 부터 <앞뒤> 기간에 쓰여진 문서를 candidate doc 으로 사용 """ test_days_len = 20 additional_days_len = 4 candidates_len = test_days_len + additional_days_len users_dict = {} with codecs.open('./res/users.json', 'rU', 'utf-8') as f: for line in f: j_map = json.loads(line) users_dict[j_map['id']] = j_map cand_docs = {} t_obj = datetime.strptime(test_end_date, "%Y%m%d") doc_deadline_date = (t_obj + timedelta(days=1)).strftime("%Y%m%d") candidate_date = (t_obj - timedelta(days=candidates_len)).strftime("%Y%m%d") doc_deadline_date = int(doc_deadline_date) * 100 candidate_date = int(candidate_date) * 100 with codecs.open('./res/metadata.json', 'rU', 'utf-8') as f: for line in f: j_map = json.loads(line) # ts 를 datetime 으로 변경 j_map['time'] = ts2time(j_map['reg_ts']) # [test 기간 + test 이전 몇 일 기간] 동안의 doc 정보 저장 if j_map['time'] < doc_deadline_date and j_map[ 'time'] > candidate_date: cand_docs[j_map['id']] = j_map print("# of candidate articles from {} to {} : {}".format( candidate_date // 100, test_end_date, len(cand_docs))) # 20190221 부터 한 달간의 클릭 문서 분포를 파악 d_obj = datetime.strptime("20190221", "%Y%m%d") date_list = [] for i in range(30): date_list.append((d_obj - timedelta(days=i)).strftime("%Y%m%d")) dist_map = get_click_dist(date_list, test_days_len, additional_days_len) s_obj = datetime.strptime("20190222", "%Y%m%d") dist_sorted_map = sorted(dist_map.items(), key=lambda k: -k[1]) click_rank_per_date = [((s_obj + timedelta(days=e[0])).strftime("%Y%m%d"), rank) for rank, e in enumerate(dist_sorted_map)] click_rank_per_date = dict(click_rank_per_date) print(click_rank_per_date) # 후보 doc 들을 writer 로 묶어줌 cand_doc_writer = {} for doc_id, doc_info in cand_docs.items(): writer = doc_info['user_id'] cand_doc_writer[writer] = cand_doc_writer.get(writer, []) + [doc_id] for k, v in cand_doc_writer.items(): c_v = [(e, int(e.split("_")[1])) for e in v] cand_doc_writer[k] = [(e[0], int(cand_docs[e[0]]['time'])) for e in sorted(c_v, key=lambda v: v[1])] user_seen = {} user_latest_seen = {} user_last_seen = {} # w2v 에 쓰일 sequences seen_seq = [] all_articles = [] # test 의 (겹치는)기간 동안의 doc 사용량 doc_cnt = {} from_dtm = 2018100100 to_dtm = 2019030100 for path, _ in tqdm(iterate_data_files(from_dtm, to_dtm), mininterval=1): for line in open(path): l = line.strip().split() user = l[0] seen = l[1:] if len(seen) > 1: seen_seq.append(seen) all_articles += seen user_seen[user] = user_seen.get(user, []) + seen date_range = path.split("./res/read/")[1] fr = int(date_range.split("_")[0]) if fr >= 2019020100: user_latest_seen[user] = user_latest_seen.get(user, []) + seen if fr < 2019022200: user_last_seen[user] = user_last_seen.get(user, []) + [fr] if fr >= 2019022200: for doc in seen: doc_cnt[doc] = doc_cnt.get(doc, 0) + 1 for u, dates in user_last_seen.items(): user_last_seen[u] = max(dates) doc_cnt = OrderedDict(sorted(doc_cnt.items(), key=lambda k: -k[1])) pop_list = [k for k, v in doc_cnt.items()][:300] del doc_cnt # word2vec 에 이용하는 데이터 만들기 vocabulary_size = len(set(all_articles)) _, _, article2idx_map, idx2article_map = \ build_dataset(all_articles, seen_seq.copy(), vocabulary_size, min_count=5, skip_window=4) filtered_vocabulary_size = len(article2idx_map) del all_articles del seen_seq print("# of vocabulary : all ({}) -> filtered ({})".format( vocabulary_size, filtered_vocabulary_size)) batch_size = 128 embedding_size = 128 num_sampled = 10 config = {} config['batch_size'] = batch_size config['embedding_size'] = embedding_size config['num_sampled'] = num_sampled config['filtered_vocaulary_size'] = filtered_vocabulary_size # word2vec ckpt 불러오기 sess = tf.Session() net = word2vec(sess, config) net.build_model() net.initialize_variables() net.restore_from_checkpoint(ckpt_path="./ckpt/", step=500000, use_latest=True) user_most_seen = {} for u, seen in user_latest_seen.items(): for doc in seen: if doc.startswith("@"): writer = doc.split("_")[0] seen_map = user_most_seen.get(u, {}) seen_map[writer] = seen_map.get(writer, 0) + 1 user_most_seen[u] = seen_map if u in user_most_seen: user_most_seen[u] = dict([ e for e in sorted(user_most_seen[u].items(), key=lambda k: -k[1]) ]) #tmp_dev = ['./tmp/dev.users.recommend', './tmp/dev.users'] #dev = ['./res/predict/dev.recommend.txt', './res/predict/dev.users'] test = ['./res/predict/recommend.txt', './res/predict/test.users'] path_list = [test] for output_path, user_path in path_list: print("Start recommendation!") print("Read data from {}".format(user_path)) print("Write data to {}".format(output_path)) ## word2vec 에 의한 top_n 먼저 계산 articles_len = 4 positives = [] with codecs.open(user_path, mode='r') as f: for idx, line in enumerate(f): u = line.rsplit()[0] pos = [ article2idx_map[e] for e in reversed(user_seen.get(u, [])) if e in article2idx_map ][:articles_len] remain_len = articles_len - len(pos) pos += [filtered_vocabulary_size for _ in range(remain_len)] positives.append(np.array(pos)) _, _, top_n_bests = net.most_similar(positives, idx2article_map=idx2article_map, top_n=300) top_n_bests = np.array(top_n_bests)[:, :, 0] with codecs.open(output_path, mode='w') as w_f: with codecs.open(user_path, mode='r') as f: for idx, line in tqdm(enumerate(f)): u = line.rsplit()[0] user_most_seen_map = user_most_seen.get(u, {}) def rerank_doc(doc_list): """ rerank : 세 가지 방식으로 doc_list 로 들어온 문서들을 재정렬함 - 우선순위 1. 유저가 과거(user_latest_seen) 에 본 에디터의 글 횟 수 -> 많을수록 우선 - 우선순위 2. 해당 날짜에 만들어진 문서가 클릭될 확률 순위(click_rank_per_date) -> rank 작을 수록 우선 - 우선순위 3. 문서가 만들어진 최신 순 """ n_doc_list = [] for e in doc_list: if e[1] > user_last_seen.get(u, 0) and str( e[1] // 100) in click_rank_per_date: writer = e[0].split("_")[0] writer_hit_cnt = user_most_seen_map.get( writer, 0) n_doc_list.append( (e[0], e[1], click_rank_per_date[str(e[1] // 100)], writer_hit_cnt)) reranked_doc_list = [ e[0] for e in sorted(n_doc_list, key=lambda k: (-k[3], k[2], k[1])) ] return reranked_doc_list ### 추천은 아래 1 + 2 + 3 순서로 함 # 1. 구독한 에디터들의 글 중들을 candidate 에서 뽑기 following_list = users_dict.get( u, {'following_list': []})['following_list'] following_doc = [] if following_list: for e in following_list: following_doc += cand_doc_writer.get(e, []) following_doc = rerank_doc(following_doc) # 2. 유저가 많이 본 에디터의 글들을 candidate 에서 뽑기 most_seen_new_doc = [] if user_most_seen_map: for e, writer_cnt in user_most_seen_map.items(): # writer 가 3 번 이상 본 경우에만 활용 if writer_cnt >= 3: most_seen_new_doc += cand_doc_writer.get(e, []) most_seen_new_doc = rerank_doc(most_seen_new_doc) # 3. word2vec 모델에서 가장 최근에 본 n 개 문서와 가장 유사한 문서들을 뽑기 positive_input = [ article2idx_map[e] for e in reversed(user_seen.get(u, [])) if e in article2idx_map ][:articles_len] if positive_input: sim_list = list(top_n_bests[idx]) else: sim_list = pop_list # 최종 추천 (1 + 2 + 3) rec_docs = following_doc + most_seen_new_doc + sim_list rec_docs = list(OrderedDict.fromkeys(rec_docs)) # 이미 유저가 과거에 본 문서는 제거 n_rec_docs = [] for d in rec_docs: if d not in user_seen.get(u, []): n_rec_docs.append(d) if len(n_rec_docs) < 100: n_rec_docs = pop_list line = "{} {}\n".format(u, ' '.join(n_rec_docs[:100])) w_f.write(line) print("Finish!")
def train(args): from_dtm = 2018100100 to_dtm = 2019030100 all_articles = [] seen_seq = [] for path, _ in tqdm(iterate_data_files(from_dtm, to_dtm), mininterval=1): for line in open(path): l = line.strip().split() user = l[0] seen = l[1:] if len(seen) > 1: seen_seq.append(seen) all_articles += seen vocabulary_size = len(set(all_articles)) new_seen, count, article2idx_map, idx2article_map = \ build_dataset(all_articles, seen_seq.copy(), vocabulary_size, min_count=args.min_count, skip_window=args.skip_window) filtered_vocabulary_size = len(article2idx_map) print('Most common words', count[:5]) print("# of sentences : all ({}) -> filtered ({})".format( len(seen_seq), len(new_seen))) print("# of vocabulary : all ({}) -> filtered ({})".format( vocabulary_size, filtered_vocabulary_size)) # Reduce momory del all_articles del seen_seq span = 2 * args.skip_window + 1 # [ skip_window target skip_window ] buffer = deque(maxlen=span) # pylint: disable=redefined-builtin skip_dummy = ['UNK'] * args.skip_window all_targets = [] all_labels = [] for sen_idx, sentence in tqdm(enumerate(new_seen), total=len(new_seen)): sentence = skip_dummy + sentence + skip_dummy buffer.extend(sentence[0:span - 1]) for doc in sentence[span - 1:]: buffer.append(doc) if buffer[args.skip_window] != 'UNK': context_words = [ w for w in range(span) if w != args.skip_window and buffer[w] != 'UNK' ] _num_sample = len(context_words) if len( context_words) < args.num_skips else args.num_skips words_to_use = random.sample(context_words, _num_sample) for j, context_word in enumerate(words_to_use): all_targets.append( article2idx_map[buffer[args.skip_window]]) all_labels.append(article2idx_map[buffer[context_word]]) t1 = time() print("Shuffling indexes...") idxes = [e for e in range(len(all_targets))] random.shuffle(idxes) all_targets = np.array(all_targets)[idxes] all_labels = np.array(all_labels)[idxes] del idxes t2 = time() print("Shuffling finished [{:.1f} s]".format(t2 - t1)) config = {} config['batch_size'] = args.batch_size config['embedding_size'] = args.embedding_size config['skip_window'] = args.skip_window config['num_skips'] = args.num_skips config['num_sampled'] = args.num_sampled config['filtered_vocaulary_size'] = filtered_vocabulary_size sess = tf.Session() net = word2vec(sess, config) net.build_model() net.initialize_variables() decay_alpha = (args.alpha - args.min_alpha) / args.num_steps alpha = args.alpha check_step = 10000 save_step = 100000 average_loss = 0 t1 = time() for step in range(args.num_steps): batch_inputs, batch_labels = generate_batch(args.batch_size, all_targets, all_labels) loss_val = net.train(batch_inputs, batch_labels, alpha=alpha) alpha -= decay_alpha average_loss += loss_val if step % check_step == 0 and step > 0: average_loss /= check_step t2 = time() print("Average loss at step {}: {:.5} [{:.1f} s]".format( step, average_loss, t2 - t1)) t1 = t2 average_loss = 0 if (step % save_step == 0 and step > 0) or step + 1 == args.num_steps: print("Store checkpoints at step {}...".format(step)) net.store_checkpoint(step=step)
import os import random import six import fire import mmh3 import tqdm from util import iterate_data_files def groupby(from_dtm, to_dtm, tm p_dir, out_path, num_chunks=10): from_dtm, to_dtm = map(str, [from_dtm, to_dtm]) fouts = {idx: open(os.path.join(tmp_dir, str(idx)), 'w') for idx in range(num_chunks)} files = sorted([path for path, _ in iterate_data_files(from_dtm, to_dtm)]) for path in tqdm.tqdm(files, mininterval=1): for line in open(path): user = line.strip().split()[0] chunk_index = mmh3.hash(user, 17) % num_chunks fouts[chunk_index].write(line) map(lambda x: x.close(), fouts.values()) with open(out_path, 'w') as fout: for chunk_idx in fouts.keys(): _groupby = {} chunk_path = os.path.join(tmp_dir, str(chunk_idx)) for line in open(chunk_path): tkns = line.strip().split() userid, seen = tkns[0], tkns[1:] _groupby.setdefault(userid, []).extend(seen)