Project/multicampus-project

쿠팡 화장품 추천

fullfish 2025. 12. 15. 09:09

문제 정의 & 목표 설계

 

 

 

 

해결하고자 하는 문제

온라인 쇼핑몰에는 방대한 리뷰데이터가 지속적으로 축적되고 있으나,

고객의 개인별 특성이 리뷰 텍스트에 충분히 반영되어 있음에도 불구하고 이를 체계적으로 분석하여 개인 맞춤형 상품 추천에 활용하는 데에는 한계가 있다.

현재의 추천 시스템은 주로 판매량, 별점 평균, 인기 순위에 의존하고 있어 개인별 니즈를 정밀하게 반영하지 못하며,

그 결과 고객은 자신에게 적합하지 않은 제품을 추천받는 경험을 하게된다.

특히 화장품은 피부 타입과 취향에 따른 만족도 편차가 크기 때문에 이러한 문제는 구매 실패와 재구매율 저하로 이어질 수 있다.

본 프로젝트는 화장품 리뷰 텍스트를 기반으로 감성 분석과 키워드 유사도 분석을 수행하여,

설명 가능한 상품 유사도 추천 시스템을 구축하는 것을 목표로 한다.


기대 효과

  • 리뷰 감성 분석을 통한 부정 이슈 탐지 및 피드백
  • 리뷰 텍스트 기반 감성 분석과 키워드 유사도를 활용한 설명 가능한 추천 결과 제공
  • 단순 별점·판매량 중심 추천 방식의 한계 보완
  • 화장품 구매 실패 감소 및 고객 만족도 향상
  • 리뷰 데이터의 실질적 비즈니스 활용 사례 제시

 

소주제

  • 이미지 유무에 따른 감성분류, 평점, 글 길이간의 상관관계 분석
  • 배송 유형에 따른 감성분류, 평점 차이 분석
  • 이 리뷰가 도움됐어요’의 개수와 감성분류, 평점, 글 길이간의 상관관계
  • 시계열 감성 지수 모니터링을 통한 부정 이슈 조기 탐지

청사진

Streamlit 대시보드에서 상품 검색 or 드롭 다운으로 선택시

선택한 상품과 리뷰 감성이 긍정적이며 유사도가 높은 화장품 추천

추천 근거가 되는 주요 키워드 및 감성 점수 시각화



데이터 수집

화장품 관련 약 60개 카테고리에서

카테고리당 5만개 정도의 리뷰를 수집

데이터 구조

{
  "search_name": "string",
  "total_collected_reviews": "int", 
  "total_text_reviews": "int",
  "total_product": "int",
  "total_rating_distribution": {
    "5": "int",
    "4": "int",
    "3": "int",
    "2": "int",
    "1": "int"
  },
  "data": [
    {
      "product_info": {
        "product_id": "string", 
        "brand": "string",
        "category_path": "string",
        "product_name": "string",
        "price": "string",
        "delivery_type": "string",
        "total_reviews": "string",
        "product_url": "string",
        "rating_distribution": {
          "5": "int",
          "4": "int",
          "3": "int",
          "2": "int",
          "1": "int"
        }
      },
      "reviews": {
        "total_count": "int",   
        "text_count": "int",   
        "data": [
          {
            "id": "int",
            "score": "int",
            "date": "string",
            "collected_at": "string",
            "nickname": "string",
            "has_image": "boolean",
            "helpful_count": "int",
            "title": "string",
            "content": "string",
            "full_text": "string"
          }
        ]
      }
    }
  ]
}

문제점

1. postman에서의 단순 get 요청은 거부 당함
-> 유저 인증 토큰 넣어서 get 요청


2. 단순 get으로 가져온 데이터는 상품 정보는 가져 오지만 리뷰데이터는 가져 오지 않음
-> 리뷰는 10개씩 끊어서 쿠팡에서 자체적으로 가져옴. 리뷰의 페이지의 다음페이지 눌렀을때 비동기 요청으로 일반 유저가 알 수 없는 api로 요청을 보내기 때문
-> 셀레니움으로 페이지 하나하나 넘기면서 리뷰 수집

 

3. 각 스크립트 끝날때 driver.quit() 하면 오류
-> 다른데서 driver 써야하는데 driver 꺠우기 전에 실행되므로 다른 함수 호출시 drive 넘겨서 재사용

 

4. 크롤링에 시간이 오래 걸림
-> ThreadPoolExecutor를 이용한 병렬 수집을 구현 했으나 병렬로 접근시 바로 접근제한됨

       -> 안전하게 직렬수집

 

5. 대량의 데이터 수집시 차단당함
-> 키워드별로 드라이버 재실행 시켜봄
-> 드라이버 1개당 6000개의 데이터 수집시 차단됨을 확인

-> 가설 1. 클릭 횟수 or 페이지 이동 어느 정도 이상이면 차단

-> 가설 2. 드라이버 켜진 시간 어느 정도 이상이면 차단

 

어느 가설이 맞는지 확인하기 위해서 딜레이를 더 늘려도 보고 줄여도 봤는데

여전히 6000개 정도 수집했을때 차단당한것으로 보아 가설 1번이 맞아보인다

->  각 상품별로 드라이버 새로 띄우고 딜레이는 줄였다

 

6. 문제 7로 인해서 상품별로 따로 드라이버 켜줬더니 드라이버 충돌이 잦음

-> 1. Garbage Collector의 모듈 설치해서 안쓰는 메모리 까지 삭제

-> 2. driver.quit()만이 아닌 del driver로 이중 삭제

-> 3. 실패시 드라이버 삭제후 같은 url로 재시도

-> 4. 메모리 정리위해 딜레이 시간 충분히 줌

으로 해결했다

 

7. 리뷰수 상품당 1500개 랜더 제한

-> postman을 써봤는데 헤더 넣어도 request앱은 차단되는거 같아서
직접 url로 써보니까

이런식으로 뜨기는 하는데

https://www.coupang.com/next-api/review?productId=5611991510&page=50&size=30&sortBy=DATE_DESC&ratingsSummary=true&ratings=&market=

이런 요청에서 기본 size=10에 page는 150페이지까지 뜨는데

size를 30으로 늘린다면 page는 50까지밖에 안뜬다 결국 1500개 제한은 백단에서 걸려있어서 해결 불가 

-> 점수별로 긁어오기. 5점 리뷰 1500개, 4점 리뷰 1500개 이런식으로 긁어오면 최대 7500개 긁어올 수 있다

 

8. 리뷰 10페이지에서 다음 페이지 이동시 10 -> 20으로 가지므로 11부터 다시 수집하게 수정

 

성능 개선

1. 상품별로 드라이버 재실행하면 딜레이가 길어짐

-> 현 상품의 메타데이터를 미리 가져와서 현재 드라이버에서 수집한 리뷰개수와 수집될 리뷰개수의 합이 6000이상이면 재실행

 

2. options.add_argument("--blink-settings=imagesEnabled=false") 옵션으로 이미지 안뜨게 해서 속도 상승

 

3. 전체 리뷰의 n%정도의 리뷰만 content가 존재함

-> 현재 각 별점별 수집할 리뷰수(REVIEW_TARGET)를 고정적으로 수집함

 각 별점별의 총 개수 * 0.n과 REVIEW_TARGET중 큰 값만큼을 각 별점별로 수집하도록 변경해서 수집속도와 content있는 리뷰의 비율을 늘림

REVIEW_TARGET는 200으로 고정하고 0.n의 값을 바꿔봄

  전체 리뷰수 내용 포함 리뷰수 내용 포함 리뷰수 비율 비고
리뷰 200개씩 고정 101338 50268 49.6% 스킨, 아이라이너
0.1 101229 52440 51.8% 로션, 마스카라
0.25 99744 53523 53.7% 에센스_세럼_앰플, 쿠션_팩트

별점 별 댓글 수집수 = max(200, 현 별점의 리뷰수 * 0.25)로 잡는게 딜레이 시간도 줄이고 양질의 데이터를 모을 수 있음

 

 

전처리

1. 데이터 클리닝 및 정규화

  • 노이즈 제거: HTML 태그와 특수문자를 제거하여 순수 텍스트만 남겼습니다.
  • 이모지 및 특수 기호 삭제: 분석에 불필요한 이모지와 별표(★) 등의 기호를 정규표현식으로 제거했습니다.
  • 감성 표현 정규화: 'ㅋㅋ', 'ㅎㅎ'와 같이 반복되는 자모음을 축소하여 데이터의 일관성을 높였습니다.
    'ㅋㅎㅋㅎㅋㅎㅋㅎ'같은것도 자모음의 경우 2개까지 하나의 묶음으로 봐서 축소시켰습니다.

2. 데이터 형식 및 자료형 변환

  • 자료형 정수화: 문자열로 되어 있던 가격, 별점, 리뷰 수, 상품 ID 등을 수치 연산이 가능하도록 정수형(int)으로 변환했습니다.
  • 날짜 및 시간 표준화: 제각각이던 날짜 형식을 ISO 표준 형식(YYYY-MM-DD)으로 통일했습니다.

3. 결측치 및 중복 처리

  • 결측치 제거: helpful_count가 0이거나 has_image가 False인 필드, 그리고 빈 문자열 필드를 삭제하여 데이터 용량을 최적화했습니다.
  • 중복 리뷰 제거: 닉네임, 날짜, 리뷰 본문을 기준으로 동일한 리뷰가 중복 수집된 경우를 찾아 삭제했습니다.
  • 데이터 분리: 텍스트 내용이 있는 리뷰와 없는 리뷰를 별도의 파일로 분리하여 분석 효율을 높였습니다.

4. 브랜드 및 카테고리 표준화

  • 브랜드명 통합: 국문/영문 혼용 브랜드명을 하나의 대표 명칭으로 통일했습니다.
  • 카테고리 그룹화: 복잡한 카테고리 경로를 '스킨', '로션' 등 60여 개의 대표 카테고리로 단순화했습니다.
  • 상품명 정제: 상품명 내에 포함된 브랜드명이나 용량(ml), 수량(개) 정보를 삭제하여 핵심 상품 속성만 남겼습니다.

5. 자연어 처리 (NLP) 기반 전처리

  • 형태소 분석 및 토큰화: Okt 분석기를 사용하여 명사, 동사, 형용사 위주로 단어를 추출했습니다.
  • 불용어(Stopwords) 제거: 분석 의미가 없는 단어들을 미리 준비한 stopwords-ko.txt 목록을 기반으로 필터링했습니다.
  • 감성 라벨링: 별점을 기준으로 4점 이상은 긍정(1), 2점 이하는 부정(0)으로 분류하는 라벨을 생성했습니다.
  • 벡터화 (Word2Vec): 리뷰 텍스트를 100차원의 수치 벡터로 변환하여 기계가 읽을 수 있는 형태로 만들었습니다.

 

문제점

1. word2vec 모델의 학습 범위

리뷰기반으로 word2vec을 하였는데 이러면 각 카테고리별로 새로운 word2vec모델을 학습함

이러면 예를들어 오일 파일에서 촉촉하다라는 단어의 벡터와 크림 파일에서의 촉촉하다라는 단어의 벡터가 다른 수치를 가지게 됨

-> 모든 파일의 리뷰 토큰을 모아서 하나의 word2vec 모델을 한 번만 학습해야함 그래야지 동일한 의미 공간 안에서 비교 가능

 

2. 개별 리뷰 단위로만 벡터 저장중

-> 상품의 대표 벡터를 product_info 영역에 추가하는게 좋음

 

1~2. 관련 해결법

300만개의 리뷰를 모두 모아 임베딩 스페이스(공통 벡터)를 생성

-> 공통 벡터를 이용하여 각각의 리뷰 벡터를 생성

리뷰 벡터

-> 각 리뷰벡터의 평균으로 상품의 대표 벡터를 생성

상품 벡터

-> 상품의 대표 벡터를 이용해서 상품관 연관성 비교

 

단순 상품 추천만 구현 한다면 각 리뷰의 벡터는 필요없음

그러나

1. 상품의 특징을 가장 잘 요약하는 대표 리뷰 선정

2. 리뷰 내 검색 및 필터링

3. 벡터를 군집화하여 '보습력에 대한 칭찬이 많다' 와 같은 통계 내기
이런것을 하려면 각 리뷰의 벡터도 필요함

 

3. 300만건의 리뷰를 koNLPy의 Okt로 분석하는건 오래걸림

-> 멀티프로세싱으로 cpu코어를 모두 활용하거나, Okt대신 더 빠른 Mecab형태소 분석기가 좋음

-> 그러나 Mecab는 정말 대용량 데이터에서 쓰는게 좋고 Okt에 비해 OS도 타고 형태소가 쓸데없이 더 세분화되서 쪼개지는 문제가 있어서 Okt유지

 

4. JSON 파일로 저장시 파일이 너무 무거워져서 읽이 힘듦

-> Apache Parquet을 이용. 벡터를 Binary로 압축하여 용량을 줄이고 열지향이라 유사도 계산에 필요한 벡터 칼럼만 골라서 메모리 올릴 수 있음(상품 검색 속도 향상)

 

5. NFC와 NFD문제

-> .parquet에 저장할때 NFD형태로 저장이되서 사람이 타이핑한 글자(NFC)로 서치가 안됐음

-> 저장 및 불러올때 NFC로 형변환

 

데이터 저장

기존에는 json파일로 저장

json은 용량이 너무 커지고 불러올 때도 로드하는 양이 많기떄문에

 

수정 1.

Aparch Parquet을 이용, parquet. 파일로 저장

이점 1. 벡터의 경우 BInary로 저장해서 문자열이 아닌 정수형으로 저장된다
  문자열의 경우에도 압축이 되므로 용량이 10~20%정도로 줄어 든다

이점 2. 열 방식으로 저장하므로 접근이 빠르다

 

수정 2. 

AWS를 이용해서 데이터를 저장해보려고함

 

TF-IDF

기본 TF-IDF 공식

기본 공식은 현재 내가 하고자하는 감성 키워드 추출하기가 어려움

이유 1. 기본 공식은 개별 문서 내에서 단어의 중요도 측정.

  현재 내가 원하는건 긍,부정 두 집단 사이에 어떤 단어가 더 특징적으로 나타나는지. 두 집단 간 점수 차이 계산 필요

이유 2. 클래스 불균형

  긍정 리뷰가 압도적으로 많아서 중립단어가 긍정쪽으로 오인됨

 

해결

 

  • diff: 긍정 리뷰와 부정 리뷰 사이의 단어 가중치 평균 차이 (감성 강도)
  • support: 해당 단어가 나타난 전체 문서 수 (데이터 신뢰도)
  • pc, nc: 긍정 및 부정 리뷰에서 해당 단어가 출현한 횟수 (Positive Count, Negative Count)
  • total_pos_reviews, total_neg_reviews: 전체 긍정 및 부정 리뷰의 총 개수

 

 

EDA

시각화

(현재 모든 데이터가 아닌 일부 데이터로만 함)

워드 클라우드의 경우 나중에 TF-IDF값 기반으로 바꿀것임

 

평가지표

 

  • 정확도 (Accuracy): 모델이 전체 데이터 중에서 긍정과 부정을 정확하게 맞춘 비율
  • 정밀도 (Precision): 모델이 긍정이라고 예측한 리뷰들 중에서 실제로 긍정인 리뷰의 비율
  • 재현율 (Recall): 실제 긍정인 리뷰들 중에서 모델이 긍정이라고 찾아낸 비율
  • F1 점수 (F1-score): 정밀도와 재현율의 조화 평균으로, 두 지표가 얼마나 균형을 이루는지 나타냅니다. 1에 가까울수록 성능이 우수함을 뜻합니다.

 

 

로지스틱 회귀

로지스틱 회귀 모델은 Word2Vec으로 수치화된 데이터를 입력받아 다음의 시그모이드(Sigmoid) 함수를 통해 해당 리뷰가 긍정(1)일 확률을 계산합니다.
긍부정 레이블 필요: 정답지를 통한 가중치 학습

필요성 1. 추천 순위의 정밀도 향상

단순 유사도가 높은것이 아닌 유사하면서 평이 좋은 상품 추천

필요성 2. 별점의 주관성 보정

별점의 노이즈(내용과 다른 별점 or 광고성 별점)를 없애고 리뷰의 감성에 따라 추천

 

    predict_sentiment(
        "나는 이것 저것 많은 제품을 썼는데 그 중에서 제일 좋다",
        verbose=True,
    )
    predict_sentiment(
        "나는 이것 저것 많은 제품을 써봤는데 대부분 별로 였어 그런데 이 제품은 매우 좋아",
        verbose=True,
    )
    
원문: 나는 이것 저것 많은 제품을 썼는데 그 중에서 제일 좋다
토큰: ['많다', '제품', '중', '제일', '좋다']
감성 점수: 0.8342
판정: 긍정

원문: 나는 이것 저것 많은 제품을 써봤는데 대부분 별로 였어 그런데 이 제품은 매우 좋아
토큰: ['많다', '제품', '대부분', '별로', '제품', '매우', '좋다']
감성 점수: 0.2058
판정: 부정

 

 

4개의 카테고리에서의
word2vec과 bert 성능 비교

정확도: 0.8890

분류 리포트:
              precision    recall  f1-score   support

       부정(0)       0.40      0.90      0.56       970
       긍정(1)       0.99      0.89      0.94     11581

    accuracy                           0.89     12551
   macro avg       0.70      0.89      0.75     12551
weighted avg       0.95      0.89      0.91     12551

--------------------------

정확도: 0.9452

분류 리포트:
              precision    recall  f1-score   support

       부정(0)       0.59      0.95      0.73       970
       긍정(1)       1.00      0.95      0.97     11581

    accuracy                           0.95     12551
   macro avg       0.79      0.95      0.85     12551
weighted avg       0.96      0.95      0.95     12551

 

 

11시간 51분 8개 카테고리

                        전체 파이프라인 완료!                        
                 종료 시간: 2026-01-13 10:55:39                 
                   총 소요 시간: 11시간 51분 12초                    
    Phase 1: 303.8초 | Phase 2: 57.6초 | Phase 3: 41953.3초  

 

비이알티(BERT)는 케이엘유이(KLUE) 데이터셋의 표준 모델로서 뉴스, 위키피디아, 청와대 국민청원 등 매우 광범위한 한국어 텍스트로 학습

 

 

전처리 페이즈1 속도 개선

Kiwi 사용함

Mecab은 너무 자잘하게 분해되어서 의미가 없게끔 분해되기도 함

 

전처리 페이즈3 속도 개선

1. fp16(반정밀도)
비트 수를 32비트에서 16비트로 줄임으로서 연산 속도 높이고 메모리 사용량을 반으로 낮춤

정밀도가 낮아질 수 있으나 0.1%정도로 큰 변화없음

2. 다이나믹 배치

문장 길이를 정렬해서 각 배치내의 문장 길이를 비슷하게 맞춤으로서 패딩 낭비 최소화

3. 최적 배치 사이즈 찾기

패딩 낭비 최소화 (Dynamic Batching 효과)
메모리 대역폭 최적 활용
SM 활용도 균형
시피유-지피유 데이터 병목 (I/O Bottleneck): 페이즈 3 과정은 시피유가 텍스트를 읽고 토큰화한 뒤 지피유로 넘겨주는 구조입니다. 배치 사이즈가 너무 커지면(예: 1024) 시피유가 데이터를 준비하는 시간이 지피유가 연산하는 시간보다 길어집니다. 지피유가 데이터를 기다리며 노는 시간이 발생하므로 전체 속도는 저하됩니다.

 

 

미세조정

Stratified Split: 라벨 비율 유지하며 학습/검증 분리 (80/20)
Early Stopping: 2 에포크 동안 개선 없으면 조기 종료
Warmup: 학습률 워밍업 (처음 10%)
Weight Decay: 과적합 방지 (0.01)
Best Model Loading: 최고 F1 스코어 모델 자동 저장

 

워크플로우

페이즈 1,2 실행(전처리 및 Parquet생성)

미세조정(커스텀 모델 생성)

커스텀모델로 벡터화(페이즈3)

감성 분류 성능 평가