Reddit PRAW API와 GPT-4o-mini를 활용한 감성 분류
여러 서브레딧의 실제 토론에서 의견을 추출하고, 대규모로 게시글과 댓글을 필터링·요약·분류하는 Reddit 감성 분석 파이프라인을 GPT-4o-mini로 구축하는 방법을 알아보세요. 본 문서는 AI 번역본입니다. 오역이 있을 경우 댓글로 알려주세요.
Created on September 12|Last edited on September 12
Comment
새로운 기술이나 논쟁적인 이슈에 대한 여론을 파악하려고 Reddit 토론을 읽어 내려가다가, 방대한 양과 정리되지 않은 대화 흐름에 압도된 적이 있나요? Reddit에는 다양한 의견이 넘쳐나지만, 이 스레드들에서 의미 있는 패턴이나 추세를 수작업으로 뽑아내기는 쉽지 않습니다. 소수의 카리스마 있는 댓글이나 높은 추천을 받은 답변에 과도하게 영향을 받아, 그것들이 전체 커뮤니티의 관점을 대변한다고 잘못 가정하기 쉽고, 조용하지만 훨씬 더 대표적일 수 있는 다수의 목소리는 간과하게 됩니다.
이 글은 자동화된 감성 분석이 Reddit 토론에서 더 명확하고 균형 잡힌 인사이트를 제공함으로써 이러한 과제를 어떻게 해결할 수 있는지 살펴봅니다. 감성 분류에 GPT-4o-mini를 활용하면 방대하고 복잡한 댓글 스레드에 숨겨진 주된 의견을 드러내어, 가장 큰 목소리에 치우치지 않고 공중의 관점을 보다 정확하게 이해할 수 있습니다.

Reddit 감성 분석 준비하기의존성 설치 및 환경 설정Reddit API 인증하기Reddit 콘텐츠 가져오기 및 처리Python으로 Reddit 감성 분류하기GPT-4o-mini로 감정 분석하기 W&B Weave로 감정 분석 추적하기 실제 적용 사례와 활용 분야결론
Reddit 감성 분석 준비하기
감성 분석에 들어가기 전에 먼저 Python 환경을 설정하고 Reddit API 인증을 완료해야 합니다. 다음 섹션에서는 의존성 설치, API 자격 증명 발급, 그리고 분석에 필요한 Reddit 관련 콘텐츠를 가져오는 과정까지 단계별로 안내합니다.
의존성 설치 및 환경 설정
먼저 Reddit 감성 분석 파이프라인에 필요한 라이브러리를 설치해 봅시다:
pip install praw litellm asyncio weave
Reddit API 인증하기
Reddit 데이터를 조회하려면 Reddit 개발자 계정을 만들고 애플리케이션을 등록해야 합니다:
1. https://www.reddit.com/prefs/apps 로 이동하세요
2. 아래쪽에서 "Create App" 또는 "Create Another App"을 클릭하세요
3. 필요한 정보를 입력하세요:
- 이름: (앱 이름, 예: "Sentiment Analyzer")
- 앱 유형으로 "script"를 선택하세요
- "redirect uri"를 http://localhost:8000으로 설정하세요
- 설명을 추가하세요
- "Create app"을 클릭하세요
- 앱 이름 아래에 있는 client ID와 client secret을 기록해 두세요

이제 PRAW를 사용해 Reddit API에 인증할 수 있습니다:
import prawREDDIT_CLIENT_ID = 'YOUR_CLIENT_ID'REDDIT_CLIENT_SECRET = 'YOUR_CLIENT_SECRET'REDDIT_USER_AGENT = 'sentiment_analyzer/0.1 by YOUR_USERNAME'# Initialize the Reddit API client - using read-only mode for simplicityreddit = praw.Reddit(client_id=REDDIT_CLIENT_ID,client_secret=REDDIT_CLIENT_SECRET,user_agent=REDDIT_USER_AGENT)# test: print the titles of the 5 hot posts in r/Pythonfor submission in reddit.subreddit('Python').hot(limit=5):print(submission.title)
Reddit 콘텐츠 가져오기 및 처리
원시 Reddit 데이터는 전통적인 감성 분석 방식에 고유한 어려움을 제시합니다. 게시글과 댓글에는 속어, 이모지, 하이퍼링크, 마크다운 서식, 인용문, 그리고 단��� 분류기를 혼란스럽게 할 수 있는 중첩 토론이 자주 포함됩니다. 과거에는 감성 분석 파이프라인이 이러한 잡음을 정리하기 위해 광범위한 전처리를 요구했습니다. 예를 들어 URL 제거, 서식 태그 제거, 대소문자 정규화, 불용어 제거, 특수 문자 처리 등이 필요했습니다.
그러나 생성형 AI 시대에는 대규모 언어 모델이 텍스트 전처리 방식 자체를 바꿔 놓았습니다. 효과적으로 작동하려면 세심한 정리가 필요했던 전통적인 NLP 파이프라인과 달리, 최신 LLM은 최소한의 전처리만으로도 원시 Reddit 콘텐츠를 이해하고 해석할 수 있습니다. 이러한 모델은 Reddit과 유사한 소셜 미디어 콘텐츠를 포함해 다양한 인터넷 텍스트로 학습되어, 플랫폼 특유의 언어적 패턴도 처리할 수 있습니다.
프로젝트의 기반이 될 주어진 subreddit에서 게시글과 댓글에 접근하는 방법을 간단히 보여 드리겠습니다:
import prawimport osimport jsonfrom datetime import datetime# Reddit API credentials# Initialize the Reddit API client - using read-only mode for simplicityREDDIT_CLIENT_ID = 'YOUR_REDDIT_CLIENT_ID'REDDIT_CLIENT_SECRET = 'YOUR_REDDIT_CLIENT_SECRET'REDDIT_USER_AGENT = 'your_app_name/0.01 by YOUR_REDDIT_USERNAME'REDDIT_USERNAME = 'YOUR_REDDIT_USERNAME'REDDIT_PASSWORD = 'YOUR_REDDIT_PASSWORD'OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")# Init Redditreddit = praw.Reddit(client_id=REDDIT_CLIENT_ID,client_secret=REDDIT_CLIENT_SECRET,user_agent=REDDIT_USER_AGENT,username=REDDIT_USERNAME,password=REDDIT_PASSWORD)def search_subreddits(topic, subreddits=None, limit=10, time_filter='month'):"""Search for posts about a specific topic across one or more subreddits.Args:topic (str): The search querysubreddits (list): List of subreddit names to search. If None, searches all of Redditlimit (int): Maximum number of posts to retrieve per subreddittime_filter (str): 'hour', 'day', 'week', 'month', 'year', or 'all'Returns:list: List of post data dictionaries"""results = []# If no specific subreddits provided, search all of Redditif not subreddits:print(f"Searching all of Reddit for: {topic}")for post in reddit.subreddit('all').search(topic, sort='relevance',time_filter=time_filter, limit=limit):post_data = extract_post_data(post)results.append(post_data)print(f"Found post: {post.title[:60]}...")else:# Search each specified subredditfor sub_name in subreddits:try:print(f"Searching r/{sub_name} for: {topic}")subreddit = reddit.subreddit(sub_name)for post in subreddit.search(topic, sort='relevance',time_filter=time_filter, limit=limit):post_data = extract_post_data(post)results.append(post_data)print(f"Found post: {post.title[:60]}...")except Exception as e:print(f"Error searching r/{sub_name}: {str(e)}")print(f"Retrieved {len(results)} posts total")return resultsdef extract_post_data(post):"""Extract relevant data from a Reddit post and its comments.Args:post: A PRAW post objectReturns:dict: Dictionary containing post data and comments"""# Extract basic post informationpost_data = {"id": post.id,"title": post.title,"body": post.selftext,"author": str(post.author),"score": post.score,"upvote_ratio": post.upvote_ratio,"url": post.url,"created_utc": post.created_utc,"created_date": datetime.fromtimestamp(post.created_utc).strftime('%Y-%m-%d %H:%M:%S'),"num_comments": post.num_comments,"subreddit": post.subreddit.display_name,"is_self": post.is_self, # True if text post, False if link post"permalink": f"https://www.reddit.com{post.permalink}","comments": []}# Get comments (limiting to top-level comments for simplicity)post.comments.replace_more(limit=0) # Remove "load more comments" objectsfor comment in post.comments[:20]: # Get top 20 commentstry:comment_data = {"id": comment.id,"author": str(comment.author),"body": comment.body,"score": comment.score,"created_utc": comment.created_utc,"created_date": datetime.fromtimestamp(comment.created_utc).strftime('%Y-%m-%d %H:%M:%S')}post_data["comments"].append(comment_data)except Exception as e:print(f"Error processing comment: {str(e)}")return post_datadef save_to_json(data, filename="reddit_data.json"):"""Save the Reddit data to a JSON file"""with open(filename, 'w', encoding='utf-8') as f:json.dump(data, f, ensure_ascii=False, indent=2)print(f"Data saved to {filename}")# Example usageif __name__ == "__main__":# Define search parametersTOPIC = "artificial intelligence"SUBREDDITS = ["technology", "MachineLearning", "futurology", "ArtificialIntelligence"]POST_LIMIT = 5 # Posts per subreddit# Search for postsresults = search_subreddits(TOPIC, SUBREDDITS, limit=POST_LIMIT)# Save data to filesave_to_json(results, f"{TOPIC.replace(' ', '_')}_reddit_data.json")
이 스크립트는 PRAW를 사용해 특정 주제에 대한 Reddit 게시글과 댓글을 수집합니다. 먼저 Reddit API로 인증을 수행한 다음, 키워드와 관련된 게시글을 Reddit 전체 또는 지정한 subreddit에서 검색하는 search_subreddits를 정의합니다. 각 게시글에 대해 다음을 호출합니다 extract_post_data 제목, 본문, 작성자, 점수, 타임스탬프, 그리고 최대 20개의 최상위 댓글 같은 필드를 가져옵니다. 이러한 항목은 딕셔너리로 저장됩니다. save_to_json 전체 데이터셋을 JSON 파일로 저장합니다. 마지막에는 선택한 subreddit들에서 “artificial intelligence”에 대한 샘플 쿼리를 실행한 뒤, 결과를 다음으로 저장합니다 artificial_intelligence_reddit_data.json. 이 출력은 감성 분석이나 기타 텍스트 처리에 사용할 수 있습니다.
Python으로 Reddit 감성 분류하기
Reddit 게시글과 댓글의 감정을 판단하기 위해, 우리는 Reddit 데이터 수집과 LLM 기반 프롬프트 분류를 결합한 스크립트를 사용합니다. 사전 구축된 감성 사전이나 기학습 분류기에 의존하는 대신, 이 방법은 자연어 프롬프트와 GPT-4o-mini를 활용해 특정 주제와의 관련 맥락에서 게시글 또는 댓글의 감정이 Good, Bad, Neutral 중 무엇인지 판별합니다. 이 접근 방식은 Reddit에서 흔한 풍자, 속어, 맥락 모호성에 대해 더 견고하게 작동합니다.
먼저 앱의 전체 스크립트를 공유한 다음, 동작 방식의 핵심 내용을 설명하겠습니다. 이 스크립트에서는 LLM 앱을 위해 설계된 경량 관측 및 로깅 도구인 Weave를 사용합니다. Weave는 어떤 프롬프트가 실행되었고 어떤 응답이 반환되었는지 추적하며, 분석 규모가 커질수록 디버깅을 더 쉽게 해 줍니다. Weave 없이도 앱을 실행할 수 있지만, 모델이 각 단계에서 어떻게 사고했는지 추적하는 데 유용합니다.
스크립트는 먼저 get_subreddits() 해당 주제가 실제로 논의되고 있을 법한 관련 커뮤니티를 식별하기 위해서입니다. 이 함수는 사용자의 쿼리를 받아, 사람들이 그 주제에 관해 자연스럽게 대화할 만한 곳을 기준으로 다섯 개의 subreddit 이름을 나열하도록 LLM에 프롬프트를 보냅니다. 해당 subreddit들이 식별되면, 스크립트는 검색어를 생성하고 각 subreddit에서 스레드를 가져옵니다. 이후에는 관련성 기준으로 게시글을 필터링합니다. 즉, 찾은 모든 내용을 분석하지는 않습니다. 대신 LLM을 사용해 각 게시글이 원래 쿼리와 얼마나 밀접하게 연관되는지 점수를 매깁니다. 관련성 임계값(기본 0.6) 아래로 떨어지는 게시글은 건너뜁니다. 이를 통해 스크립트가 명확히 온토픽인 스레드에만 감성 분류를 수행하도록 보장합니다. 필터링이 끝나면 긴 게시글은 요약하고, 메인 게시글과 제한된 수의 최상위 댓글에 대해 감성을 분류하며, 출력은 단계적으로 표시됩니다. 실행은 게시글과 댓글 전반의 Good, Neutral, Bad 의견 분포와 지배적 감정에 대한 신뢰도 점수를 함께 제시하며 마무리됩니다.
import osimport reimport asyncioimport prawfrom litellm import acompletionfrom collections import Counterimport weave; weave.init('reddit_sentiment')# Reddit credsREDDIT_CLIENT_ID = 'YOUR_REDDIT_CLIENT_ID'REDDIT_CLIENT_SECRET = 'YOUR_REDDIT_CLIENT_SECRET'REDDIT_USER_AGENT = 'your_app_name/0.01 by YOUR_USERNAME'REDDIT_USERNAME = 'YOUR_USERNAME'REDDIT_PASSWORD = 'YOUR_PASSWORD'OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")# Init Redditreddit = praw.Reddit(client_id=REDDIT_CLIENT_ID,client_secret=REDDIT_CLIENT_SECRET,user_agent=REDDIT_USER_AGENT,username=REDDIT_USERNAME,password=REDDIT_PASSWORD)@weave.opasync def get_subreddits(query, num=2):"""Get relevant subreddits for a query"""prompt = f"""List {num} relevant subreddits where people would discuss and answer the following question:{query}Return only the subreddit names, no hashtags or explanations."""res = await acompletion(model="gpt-4o-mini",api_key=OPENAI_API_KEY,messages=[{"role": "user", "content": prompt}],temperature=0.5,max_tokens=200,)content = res["choices"][0]["message"]["content"]return [re.sub(r"^\d+[\.\)]\s*", "", line.strip().replace("r/", "").replace("/r/", ""))for line in content.splitlines() if line.strip()]@weave.opasync def summarize_post(post_title, post_body):"""Summarize a post into 30-50 words"""if len(post_body) < 200: # If post is already short, no need to summarizereturn post_title + " " + post_bodyprompt = f"""Summarize the following Reddit post in 30-50 words, capturing the main points and sentiment:Title: {post_title}Body: {post_body}Provide ONLY the summary."""try:res = await acompletion(model="gpt-4o-mini",api_key=OPENAI_API_KEY,messages=[{"role": "user", "content": prompt}],temperature=0.3,max_tokens=100,)summary = res["choices"][0]["message"]["content"].strip()return summaryexcept Exception as e:print(f"Error summarizing post: {str(e)}")# Fallback: just use the title and first 40 words of bodywords = post_body.split()short_body = " ".join(words[:40]) + ("..." if len(words) > 40 else "")return post_title + " " + short_body@weave.opasync def generate_search_terms(query):"""Generate effective search terms from the query"""prompt = f"""Generate 3 effective Reddit search terms for finding discussions about this question:{query}The terms should be effective for Reddit's search function (keywords, not full sentences).Return only the search terms, one per line, no numbering or explanations."""res = await acompletion(model="gpt-4o-mini",api_key=OPENAI_API_KEY,messages=[{"role": "user", "content": prompt}],temperature=0.7,max_tokens=100,)content = res["choices"][0]["message"]["content"]return [line.strip() for line in content.splitlines() if line.strip()]async def assess_post_relevance(post_title, post_body, topic):"""Determine if a post is relevant to the given topic"""prompt = f"""Assess if the following Reddit post is relevant to this topic: "{topic}"Post title: "{post_title}"Post body: "{post_body}"Respond with a number between 0 and 1 indicating relevance:0 = Not at all relevant to the topic0.5 = Somewhat relevant1 = Highly relevant to the topicRespond with only a number between 0 and 1."""try:res = await acompletion(model="gpt-4o-mini",api_key=OPENAI_API_KEY,messages=[{"role": "user", "content": prompt}],temperature=0,max_tokens=10,)score_text = res["choices"][0]["message"]["content"].strip()try:score = float(score_text)return scoreexcept ValueError:# If we can't parse as float, make a rough estimate based on textif "1" in score_text:return 1.0elif "0.5" in score_text:return 0.5else:return 0.0except Exception:return 0.5 # Default to maybe relevant on errorasync def fetch_threads(subs, topic, limit=5, comments_per_post=7):"""Fetch threads related to the topic, filtering for relevance"""results = []processed_urls = set() # Track processed posts to avoid duplicates# Generate search terms from the topicsearch_terms = await generate_search_terms(topic)print(f"Search terms generated: {search_terms}")# Extract keywords for fallbacktopic_keywords = topic.lower().strip()search_terms.append(topic_keywords)for sub in subs:try:for term in search_terms:print(f"Searching r/{sub} for: {term}")for post in reddit.subreddit(sub).search(term, limit=limit, sort='relevance'):# Skip if we've already processed this postif post.permalink in processed_urls:continueprocessed_urls.add(post.permalink)# Check if post is relevant to the topicrelevance_score = await assess_post_relevance(post.title, post.selftext, topic)if relevance_score < 0.6: # Threshold for relevanceprint(f"Skipping irrelevant post: {post.title} (relevance: {relevance_score:.2f})")continue# Generate a concise summary of the postpost_summary = await summarize_post(post.title, post.selftext)post.comments.replace_more(limit=0)comments = []for c in post.comments[:comments_per_post]:comments.append({"text": c.body,"score": c.score})if comments:results.append({"subreddit": sub,"title": post.title,"body": post.selftext,"summary": post_summary,"url": f"https://www.reddit.com{post.permalink}","post_score": post.score,"comments": comments,"relevance_score": relevance_score})print(f"Added relevant post: {post.title} (relevance: {relevance_score:.2f})")print(f"Summary: {post_summary}\n")except Exception as e:print(f"Error processing subreddit {sub}: {str(e)}")continuereturn results@weave.opasync def analyze_post_sentiment(post, query):"""Analyze post sentiment towards a specific topic"""# Use the summary instead of the full body if availableif "summary" in post and post["summary"]:combined_text = post["summary"]else:combined_text = f"Title: {post['title']}\nBody: {post['body']}"# If text or query is missing, return Neutralif not (combined_text and query):return "Neutral"prompt = f"""You are a sentiment classifier analyzing a Reddit post to determine opinions about a specific topic.Topic: "{query}"Post: "{combined_text}"Classify the sentiment ONLY if the post directly discusses the topic:- "Good": The post clearly expresses positive sentiment about the topic- "Bad": The post clearly expresses negative sentiment about the topic- "Neutral": The post does not clearly discuss the topic, expresses mixed feelings, or the relevance to the topic is unclearIf you're uncertain whether the post is directly relevant to the topic, classify as "Neutral".Respond with exactly one word - either "Good", "Bad", or "Neutral"."""try:res = await acompletion(model="gpt-4o-mini",api_key=OPENAI_API_KEY,messages=[{"role": "user", "content": prompt}],temperature=0,max_tokens=10,)label = res["choices"][0]["message"]["content"].strip()# Normalize output to ensure we get one of our three labelsif "good" in label.lower():return "Good"elif "bad" in label.lower():return "Bad"else:return "Neutral"except Exception:return "Neutral" # Return Neutral on any error@weave.opasync def analyze_comment_sentiment(comment, post_context, query):"""Analyze comment sentiment towards a specific topic with post context"""# Use the summary instead of the full post content if availableif "summary" in post_context and post_context["summary"]:post_context_text = post_context["summary"]else:post_context_text = f"Post title: {post_context['title']}\nPost body: {post_context['body']}"# If any required parameter is missing, return Neutralif not (comment["text"] and post_context_text and query):return "Neutral"prompt = f"""You are a sentiment classifier analyzing a Reddit comment to determine opinions about a specific topic. Dont classify the post, ONLY the commentTopic: "{query}"Original Post Context: "{post_context_text}"The Comment to classify: "{comment["text"]}"Classify the sentiment ONLY if the comment directly discusses the topic:- "Good": The comment clearly expresses positive sentiment about the topic- "Bad": The comment clearly expresses negative sentiment about the topic- "Neutral": The comment does not clearly discuss the topic, expresses mixed feelings, or the relevance to the topic is unclearIf you're uncertain whether the comment is directly relevant to the topic, classify as "Neutral".Respond with exactly one word - either "Good", "Bad", or "Neutral"."""try:res = await acompletion(model="gpt-4o-mini",api_key=OPENAI_API_KEY,messages=[{"role": "user", "content": prompt}],temperature=0,max_tokens=10,)label = res["choices"][0]["message"]["content"].strip()# Normalize output to ensure we get one of our three labelsif "good" in label.lower():return "Good"elif "bad" in label.lower():return "Bad"else:return "Neutral"except Exception:return "Neutral" # Return Neutral on any errorasync def main():query = input("Enter a topic to analyze sentiment on Reddit (e.g., 'ChatGPT for coding'): ")print(f"Topic: {query}")print("Finding relevant subreddits...")subreddits = await get_subreddits(query)print(f"Subreddits found: {subreddits}")print("Fetching and filtering relevant threads...")threads = await fetch_threads(subreddits, query)print(f"Found {len(threads)} relevant threads\n")if not threads:print("No relevant threads found. Try a different topic.")return# Track all sentiment labelspost_labels = []comment_labels = []all_labels = []# Analyze each threadfor thread in threads:print(f"\nAnalyzing: {thread['title']} [Score: {thread['post_score']}, Relevance: {thread.get('relevance_score', 'N/A')}]")print(f" → {thread['url']}")# Analyze post sentimentpost_sentiment = await analyze_post_sentiment(thread, query)post_labels.append(post_sentiment)all_labels.append(post_sentiment)print(f"Post sentiment about topic: [{post_sentiment}]")# Post summaryprint(f"Post summary: {thread.get('summary', 'N/A')}")# Analyze commentsprint("\nComments:")for c in thread["comments"]:comment_sentiment = await analyze_comment_sentiment(c, thread, query)comment_labels.append(comment_sentiment)all_labels.append(comment_sentiment)# Comment previewtext_preview = c["text"][:100] + "..." if len(c["text"]) > 100 else c["text"]print(f"[{comment_sentiment}] (score: {c['score']}) {text_preview}")# Generate sentiment summariespost_count = Counter(post_labels)comment_count = Counter(comment_labels)overall_count = Counter(all_labels)print("\n" + "="*50)print(f"SENTIMENT ANALYSIS FOR: {query}")print("="*50)print("\nPost Sentiment:")for k in ["Good", "Neutral", "Bad"]:pct = (post_count.get(k, 0) / max(len(post_labels), 1)) * 100print(f" {k}: {post_count.get(k, 0)} ({pct:.1f}%)")print("\nComment Sentiment:")for k in ["Good", "Neutral", "Bad"]:pct = (comment_count.get(k, 0) / max(len(comment_labels), 1)) * 100print(f" {k}: {comment_count.get(k, 0)} ({pct:.1f}%)")print("\nOverall Sentiment:")for k in ["Good", "Neutral", "Bad"]:pct = (overall_count.get(k, 0) / max(len(all_labels), 1)) * 100print(f" {k}: {overall_count.get(k, 0)} ({pct:.1f}%)")# Calculate relevant sentiment (excluding neutral)relevant_counts = {k: v for k, v in overall_count.items() if k != "Neutral"}if relevant_counts:dominant_sentiment = max(relevant_counts.items(), key=lambda x: x[1])[0]dominant_count = relevant_counts[dominant_sentiment]total_relevant = sum(relevant_counts.values())confidence = (dominant_count / total_relevant) * 100print(f"\nOverall sentiment about '{query}': {dominant_sentiment}")print(f"Confidence: {confidence:.1f}% (based on {total_relevant} relevant opinions)")else:print(f"\nNot enough relevant opinions about '{query}' were found.")# Log to Weights & Biasesif __name__ == "__main__":asyncio.run(main())
전체 스크립트를 갖추면, 주제 입력부터 감정 분포 요약까지 전체 파이프라인이 실행됩니다. 먼저 GPT-4o-mini를 사용해 해당 주제가 활발히 논의되고 있을 법한 subreddit을 제안합니다. 그다음 검색어를 생성하고 각 subreddit에서 게시글을 수집한 뒤, 모델이 생성한 관련성 점수를 이용해 무관한 항목을 필터링합니다.
관련성이 있는 게시글이 길다면 요약한 뒤 감성을 분석합니다. 스크립트는 또한 설정된 개수의 최상위 댓글에 대해서도 감정을 분류합니다.이는 게시글에 대한 직접 답글입니다, 다른 댓글에 대한 답글이 아니라 이 댓글에 대한 직접적인 응답만 번역합니다. 이렇게 하면 분석이 모든 곁가지 스레드로 퍼지지 않고 핵심 토론에 집중할 수 있습니다.
각 게시글과 댓글은 해당 주제에 대한 의견을 얼마�� 명확하게 표현하는지에 따라 Good, Bad, 또는 Neutral로 라벨링됩니다. 마지막에는 모든 게시글과 댓글 전반의 감정 분포가 출력되며, 지배적인 감정이 얼마나 우세한지를 나타내는 신뢰도 점수도 함께 표시됩니다.

이 문제를 해결하기 위해 전처리 단계로 게시글 요약을 추가했습니다. 전체 본문을 그대로 컨텍스트로 넘기는 대신, 스크립트가 각 게시글을 30–50단어로 요약합니다. 이렇게 하면 불필요한 노이즈를 제거하면서도 분류기가 댓글이 무엇에 반응했는지 이해할 수 있을 만큼의 맥락은 충분히 유지됩니다:
async def summarize_post(post_title, post_body):if len(post_body) < 200:return post_title + " " + post_bodyprompt = f"""Summarize the following Reddit post in 30-50 words, capturing the main points and sentiment:Title: {post_title}Body: {post_body}Provide ONLY the summary."""try:res = await acompletion(model="gpt-4o-mini",api_key=OPENAI_API_KEY,messages=[{"role": "user", "content": prompt}],temperature=0.3,max_tokens=100,)summary = res["choices"][0]["message"]["content"].strip()return summaryexcept Exception as e:words = post_body.split()short_body = " ".join(words[:40]) + ("..." if len(words) > 40 else "")return post_title + " " + short_body
먼저 게시글을 요약하면 전체 파이프라인이 더 빨라지고 모델 성능도 향상됩니다. 짧은 요약은 의미를 유지하면서도 모델에 더 깔끔하고 집중된 컨텍스트를 제공합니다. 게시글이 이미 짧다면 요약을 생략하고 원문 텍스트를 그대로 사용합니다.
GPT-4o-mini로 감정 분석하기
실제 감정을 분석하기 위해 스크립트는 GPT-4o-mini를 활용한 프롬프트 기반 분류를 사용합니다. 이는 게시글과 댓글에 대해 각각 별도로 수행됩니다. 모델은 사용자 제공 주제에 대해 텍스트가 명확한 의견을 표현하는지에 따라 Good, Bad, 또는 Neutral 중 하나의 라벨만으로 응답하도록 지시됩니다.
게시글의 경우 분류기는 주제와 함께 요약문(짧으면 원문)을 입력으로 받습니다. 프롬프트에서는 게시글이 해당 주제를 명확히 다루는 경우에만 감정을 분류하도록 모델에 명시적으로 지시합니다. 불명확하거나 무관한 경우 기본값으로 “Neutral”을 사용해야 합니다. 이렇게 하면 모호하거나 주제에서 벗어난 콘텐츠가 의견 표현으로 잘못 분류되어 발생할 수 있는 거짓 양성을 피할 수 있습니다.
async def analyze_post_sentiment(post, query):if "summary" in post and post["summary"]:combined_text = post["summary"]else:combined_text = f"Title: {post['title']}\nBody: {post['body']}"if not (combined_text and query):return "Neutral"prompt = f"""You are a sentiment classifier analyzing a Reddit post to determine opinions about a specific topic.Topic: "{query}"Post: "{combined_text}"Classify the sentiment ONLY if the post directly discusses the topic:- "Good": The post clearly expresses positive sentiment about the topic- "Bad": The post clearly expresses negative sentiment about the topic- "Neutral": The post does not clearly discuss the topic, expresses mixed feelings, or the relevance to the topic is unclearIf you're uncertain whether the post is directly relevant to the topic, classify as "Neutral".Respond with exactly one word - either "Good", "Bad", or "Neutral"."""try:res = await acompletion(model="gpt-4o-mini",api_key=OPENAI_API_KEY,messages=[{"role": "user", "content": prompt}],temperature=0,max_tokens=10,)label = res["choices"][0]["message"]["content"].strip()if "good" in label.lower():return "Good"elif "bad" in label.lower():return "Bad"else:return "Neutral"except Exception:return "Neutral"
이 함수는 게시글의 요약 또는 전체 본문을 사용해 프롬프트를 구성하고, 게시글이 주제를 명확히 다룰 때에만 감정 분류를 수행하도록 모델에 요청합니다. 주제와 무관하거나 모호하면 “Neutral”로 라벨링합니다. 이렇게 하면 모델이 일반적인 Reddit 콘텐츠를 과도하게 해석하는 것을 방지할 수 있습니다.
댓글의 경우에도 분석 방식은 비슷하지만 맥락이 한 겹 더해집니다. 감정 분류기는 댓글과 함께, 해당 댓글이 달린 원 게시글의 요약까지 함께 확인합니다:
async def analyze_comment_sentiment(comment, post_context, query):if "summary" in post_context and post_context["summary"]:post_context_text = post_context["summary"]else:post_context_text = f"Post title: {post_context['title']}\nPost body: {post_context['body']}"if not (comment["text"] and post_context_text and query):return "Neutral"prompt = f"""You are a sentiment classifier analyzing a Reddit comment to determine opinions about a specific topic. Don't classify the post, ONLY the commentTopic: "{query}"Original Post Context: "{post_context_text}"The Comment to classify: "{comment["text"]}"Classify the sentiment ONLY if the comment directly discusses the topic:- "Good": The comment clearly expresses positive sentiment about the topic- "Bad": The comment clearly expresses negative sentiment about the topic- "Neutral": The comment does not clearly discuss the topic, expresses mixed feelings, or the relevance to the topic is unclearIf you're uncertain whether the comment is directly relevant to the topic, classify as "Neutral".Respond with exactly one word - either "Good", "Bad", or "Neutral"."""try:res = await acompletion(model="gpt-4o-mini",api_key=OPENAI_API_KEY,messages=[{"role": "user", "content": prompt}],temperature=0,max_tokens=10,)label = res["choices"][0]["message"]["content"].strip()if "good" in label.lower():return "Good"elif "bad" in label.lower():return "Bad"else:return "Neutral"except Exception:return "Neutral"
스크립트를 실행하면 결과가 콘솔에 출력됩니다:

W&B Weave로 감정 분석 추적하기
Weave는 스크립트 전반에서 실행 중 애플리케이션의 성능을 모니터링하는 데 사용됩니다. 서브레딧 발견, 요약, 관련성 점수 산정, 감정 분류 등 모델 호출마다 자동으로 로그가 기록됩니다. 이를 통해 각 단계의 입력과 출력을 쉽게 점검하고, 문제가 발생할 수 있는 지점을 역추적할 수 있습니다.
개발 과정에서 Weave는 관련성 점수 산정과 감정 분류를 디버깅하는 데 특히 유용했습니다. 특정 프롬프트가 어떻게 처리되는지 확인하면서, 모델이 주제와 무관한 게시글을 오분류하거나 입력의 불필요한 부분에 영향을 받는 사례를 빠르게 찾아낼 수 있었습니다. 이를 통해 프롬프트 표현을 조정하여 출력의 일관성을 높일 수 있었습니다.
Weave 내부에서는 함수별로 손쉽게 필터링하고, 모델에 대한 각 입력과 출력을 빠르게 분석할 수 있습니다. 이는 궁극적으로 모델의 동작을 전체적으로 시각화해 주는 LLM “디버거” 역할을 합니다.

주어진 함수로 필터링한 뒤에는 각 함수가 어떻게 동작하는지 정확히 확인할 수 있습니다:

Weave는 개발 단계에서만 유용한 것이 아니라, 애플리케이션이 프로덕션에서 실행될 때도 가시성을 제공합니다. 모든 프롬프트, 모델 응답, 그리고 앱이 내리는 결정을 실시간으로 점검할 수 있습니다. 즉, 이상 징후가 보일 때 추측할 필요 없이, 모델이 무엇을 보았고 왜 그렇게 응답했는지 정확히 추적할 수 있습니다.
실제 적용 사례와 활용 분야
감정 분석은 실제 활용 분야 전반에서 분명한 가치를 제공합니다. 기업의 경우 Reddit 감정을 추적하면 브랜드 이슈, 고객 불만, PR 리스크의 초기 징후를 사전에 포착해 확산 전에 대응할 수 있습니다. 팀은 자발적인 대화의 톤을 분석함으로써 기능 출시, 가격 변경, 예기치 않은 장애에 대한 사용자 반응을 모니터링할 수 있습니다.
제품 피드백에도 유용합니다. Reddit 스레드에는 전통적인 사용자 설문에서는 드러나지 않는 솔직하고 구체적인 의견이 자주 담겨 있습니다. 해당 스레드 전반의 감정을 분류하면, 팀은 칭찬과 비판의 흐름을 파악해 개선 사항의 우선순위를 정하거나 출시가 어떻게 받아들여지고 있는지 이해할 수 있습니다.
하지만 Reddit 콘텐츠에는 태생적인 편향이 있다는 점을 유의해야 합니다. 이 플랫폼은 특정 커뮤니티와 사용자 유형에 치우치는 경향이 있으며, Reddit에서의 감정이 항상 더 넓은 대중의 여론과 일치하는 것은 아닙니다. 이런 분석에서 얻은 인사이트는 전체를 대변하기보다는 하나의 관점으로 받아들여야 합니다.
결론
Reddit는 온라인에서 가장 활발하고 다양한 실질적 토론의 장 중 하나입니다. 사람들은 거의 모든 상상 가능한 주제에 걸쳐 경험을 공유하고, 피드백을 제공하며, 질문을 하고, 아이디어를 토론합니다. 올바른 접근을 적용하면 이 콘텐츠는 강력한 인사이트의 원천이 됩니다.
GPT-4o-mini에 관련성 필터링, 요약, 감정 분류를 결합하면, 이 파이프라인은 Reddit 스레드를 구조화된 데이터로 전환할 수 있습니다. 이미 진행 중인 대화를 바탕으로 제품, 서비스, 아이디어 등 특정 주제에 대해 사용자가 어떻게 반응하는지 빠르게 파악할 수 있습니다. 결과가 플랫폼 사용자층의 편향을 반영하긴 하지만, 서로 다른 커뮤니티 전반에서 대중의 반응을 신속하고 유연하게 가늠하는 방법을 제공합니다.
Add a comment