Analisis sentimen adalah salah satu teknik NLP (Natural Language Processing) yang paling sering dipakai untuk memahami opini publik, misalnya dari komentar YouTube, media sosial, atau ulasan produk. Pada tutorial ini, kita akan membangun sistem analisis sentimen untuk komentar YouTube menggunakan model IndoBERT, mulai dari mengambil komentar lewat YouTube Data API, melabeli data secara otomatis, melakukan fine-tuning model, sampai membangun aplikasi web sederhana dengan Flask.
Studi kasus yang dipakai adalah komentar YouTube seputar BPJS Kesehatan, tapi alur ini bisa dipakai untuk topik apa pun.
Daftar isi
1. Pendahuluan
IndoBERT adalah model bahasa berbasis arsitektur BERT yang dilatih khusus untuk Bahasa Indonesia. Model ini sangat cocok untuk berbagai tugas NLP seperti klasifikasi teks, termasuk analisis sentimen, karena sudah memahami konteks dan struktur Bahasa Indonesia dengan baik.
Alur besar yang akan kita lakukan:
- Membuat YouTube Data API key
- Scraping komentar YouTube menggunakan API tersebut (hasil: file Excel/CSV)
- Memberi label otomatis (Positif/Negatif/Netral) menggunakan kamus sentimen
- Fine-tuning model IndoBERT dengan dataset yang sudah berlabel
- Membangun aplikasi web sederhana untuk memakai model tersebut
2. Gambaran Arsitektur
Secara garis besar, alurnya terbagi menjadi dua fase yang terpisah:
[FASE 1 - Google Colab] [FASE 2 - Aplikasi Web]
Scraping komentar (YouTube API)
|
Dataset mentah (CSV/Excel)
|
Auto-labeling (lexicon)
|
Fine-tuning IndoBERT ---> Folder "saved_model/"
| |
Evaluasi model Flask App (app.py)
| |
Download model (.zip) Prediksi teks & CSV
Fase 1 dikerjakan sekali saja di Google Colab (karena butuh GPU untuk training). Hasilnya berupa folder model yang tinggal dipakai berkali-kali di aplikasi web tanpa perlu training ulang.
3. Membuat YouTube Data API Key
Untuk mengambil komentar dari YouTube secara otomatis, kita perlu mengakses YouTube Data API v3 milik Google. Layanan ini gratis dengan kuota harian tertentu (cukup untuk kebutuhan tutorial/skripsi). Berikut langkah membuat API key-nya:
- Buka Google Cloud Console dan login dengan akun Google kamu.
- Klik dropdown project di bagian atas, lalu klik "New Project". Beri nama bebas, misalnya
youtube-sentiment, lalu klik Create. - Setelah project dibuat dan terpilih, buka menu APIs & Services > Library (bisa dicari lewat search bar di atas).
- Cari "YouTube Data API v3", klik hasilnya, lalu klik tombol Enable.
- Setelah aktif, buka menu APIs & Services > Credentials.
- Klik Create Credentials > API key. Google akan langsung menghasilkan sebuah API key.
- Copy API key tersebut dan simpan baik-baik — ini yang akan dipakai di kode Colab.
4. Scraping Komentar YouTube dengan Google Colab
Setelah API key siap, kita bisa langsung mengambil komentar dari video YouTube menggunakan Google Colab. Hasilnya akan disimpan dalam format Excel/CSV yang siap dipakai untuk proses labeling.
Install library yang dibutuhkan
!pip install google-api-python-client pandas openpyxl
Setup API key & fungsi ambil video ID dari URL
from googleapiclient.discovery import build
import pandas as pd
import re
API_KEY = "MASUKKAN_API_KEY_KAMU_DI_SINI"
youtube = build("youtube", "v3", developerKey=API_KEY)
def get_video_id(url):
"""Ambil video ID dari berbagai format URL YouTube"""
patterns = [
r"(?:v=|\/)([0-9A-Za-z_-]{11}).*",
r"youtu\.be\/([0-9A-Za-z_-]{11})"
]
for pattern in patterns:
match = re.search(pattern, url)
if match:
return match.group(1)
return None
Fungsi untuk mengambil semua komentar (dengan pagination)
def get_comments(video_id, max_comments=1000):
comments = []
next_page_token = None
while len(comments) < max_comments:
request = youtube.commentThreads().list(
part="snippet",
videoId=video_id,
maxResults=100,
pageToken=next_page_token,
textFormat="plainText"
)
response = request.execute()
for item in response["items"]:
snippet = item["snippet"]["topLevelComment"]["snippet"]
comments.append({
"author": snippet["authorDisplayName"],
"text": snippet["textDisplay"],
"like": snippet["likeCount"],
"time": snippet["publishedAt"],
"video_id": video_id
})
next_page_token = response.get("nextPageToken")
if not next_page_token:
break
return comments[:max_comments]
Scraping dari beberapa video sekaligus
video_urls = [
"https://www.youtube.com/watch?v=VIDEO_ID_1",
"https://www.youtube.com/watch?v=VIDEO_ID_2",
"https://www.youtube.com/watch?v=VIDEO_ID_3",
]
all_comments = []
for url in video_urls:
video_id = get_video_id(url)
if not video_id:
print(f"URL tidak valid: {url}")
continue
print(f"Mengambil komentar dari video: {video_id}")
comments = get_comments(video_id, max_comments=500)
all_comments.extend(comments)
print(f" -> {len(comments)} komentar berhasil diambil")
df = pd.DataFrame(all_comments)
print(f"\nTotal komentar terkumpul: {len(df)}")
df.head()
Simpan hasil scraping ke Excel
df.to_excel("dataset_youtube_komentar.xlsx", index=False)
from google.colab import files
files.download("dataset_youtube_komentar.xlsx")
dataset_youtube_komentar.xlsx inilah yang selanjutnya dipakai sebagai dataset mentah untuk proses auto-labeling di langkah berikutnya.
try-except di sekitar pemanggilan get_comments() jika ingin scraping banyak video sekaligus tanpa proses berhenti di tengah jalan.
5. Auto-Labeling Dataset
Karena dataset belum memiliki label sentimen, kita akan memberi label otomatis menggunakan kamus sentimen InSet (Indonesian Sentiment Lexicon). Setiap kata pada kamus punya bobot sentimen (positif/negatif), lalu skor total per komentar dihitung untuk menentukan labelnya.
Jalankan langkah berikut di Google Colab:
Install dependencies & download kamus
!pip install pandas openpyxl requests
import requests
pos_url = "https://raw.githubusercontent.com/fajri91/InSet/master/positive.tsv"
neg_url = "https://raw.githubusercontent.com/fajri91/InSet/master/negative.tsv"
with open("positive.tsv", "w") as f:
f.write(requests.get(pos_url).text)
with open("negative.tsv", "w") as f:
f.write(requests.get(neg_url).text)
Load kamus & dataset
import pandas as pd
pos_df = pd.read_csv("positive.tsv", sep="\t", header=None, names=["word", "weight"])
pos_df["weight"] = pd.to_numeric(pos_df["weight"], errors="coerce")
neg_df = pd.read_csv("negative.tsv", sep="\t", header=None, names=["word", "weight"])
neg_df["weight"] = pd.to_numeric(neg_df["weight"], errors="coerce")
pos_dict = dict(zip(pos_df["word"].str.lower(), pos_df["weight"].astype(float)))
neg_dict = dict(zip(neg_df["word"].str.lower(), neg_df["weight"].astype(float)))
from google.colab import files
uploaded = files.upload() # upload file dataset kamu
df = pd.read_excel(list(uploaded.keys())[0])
Preprocessing & fungsi labeling
import re
def preprocess(text):
text = str(text).lower()
text = re.sub(r"http\S+", "", text)
text = re.sub(r"@\w+", "", text)
text = re.sub(r"#\w+", "", text)
text = re.sub(r"[^\w\s]", " ", text)
text = re.sub(r"\d+", "", text)
text = re.sub(r"\s+", " ", text).strip()
return text
def get_sentiment(text):
clean = preprocess(text)
words = clean.split()
score = 0
for word in words:
if word in pos_dict:
score += float(pos_dict[word])
if word in neg_dict:
score += float(neg_dict[word])
if score > 0:
return "Positif", score
elif score < 0:
return "Negatif", score
else:
return "Netral", score
Jalankan labeling & export hasil
from tqdm import tqdm
tqdm.pandas()
df[["label", "sentiment_score"]] = df["text"].progress_apply(
lambda x: pd.Series(get_sentiment(x))
)
print(df["label"].value_counts())
df.to_excel("dataset_labeled.xlsx", index=False)
files.download("dataset_labeled.xlsx")
6. Fine-Tuning Model IndoBERT
Setelah dataset berlabel siap, langkah selanjutnya adalah melatih ulang (fine-tuning) model IndoBERT supaya bisa mengklasifikasikan sentimen sesuai dataset kita. Pastikan runtime Colab menggunakan GPU (Runtime > Change runtime type > GPU).
6.1 Persiapan Data & Tokenizer
!pip install transformers datasets torch scikit-learn openpyxl
from google.colab import files
import pandas as pd
from sklearn.model_selection import train_test_split
uploaded = files.upload() # upload dataset_labeled.xlsx
df = pd.read_excel(list(uploaded.keys())[0])
df = df.dropna(subset=["text", "label"])
label2id = {"Positif": 0, "Negatif": 1, "Netral": 2}
id2label = {v: k for k, v in label2id.items()}
df["label_id"] = df["label"].map(label2id)
train_df, temp_df = train_test_split(df, test_size=0.2, random_state=42, stratify=df["label_id"])
val_df, test_df = train_test_split(temp_df, test_size=0.5, random_state=42, stratify=temp_df["label_id"])
import torch
from transformers import AutoTokenizer
from torch.utils.data import Dataset
MODEL_NAME = "indobenchmark/indobert-base-p1"
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
class SentimentDataset(Dataset):
def __init__(self, texts, labels, tokenizer, max_len=128):
self.texts = texts.tolist()
self.labels = labels.tolist()
self.tokenizer = tokenizer
self.max_len = max_len
def __len__(self):
return len(self.texts)
def __getitem__(self, idx):
encoding = self.tokenizer(
str(self.texts[idx]),
max_length=self.max_len,
padding="max_length",
truncation=True,
return_tensors="pt"
)
return {
"input_ids": encoding["input_ids"].squeeze(),
"attention_mask": encoding["attention_mask"].squeeze(),
"labels": torch.tensor(self.labels[idx], dtype=torch.long)
}
train_dataset = SentimentDataset(train_df["text"], train_df["label_id"], tokenizer)
val_dataset = SentimentDataset(val_df["text"], val_df["label_id"], tokenizer)
test_dataset = SentimentDataset(test_df["text"], test_df["label_id"], tokenizer)
6.2 Training & Evaluasi Model
from transformers import AutoModelForSequenceClassification, TrainingArguments, Trainer
import numpy as np
from sklearn.metrics import accuracy_score, f1_score, classification_report
model = AutoModelForSequenceClassification.from_pretrained(
MODEL_NAME, num_labels=3, id2label=id2label, label2id=label2id
)
def compute_metrics(eval_pred):
logits, labels = eval_pred
preds = np.argmax(logits, axis=-1)
return {
"accuracy": accuracy_score(labels, preds),
"f1": f1_score(labels, preds, average="weighted")
}
training_args = TrainingArguments(
output_dir="./results",
num_train_epochs=5,
per_device_train_batch_size=16,
per_device_eval_batch_size=16,
weight_decay=0.01,
eval_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
metric_for_best_model="f1",
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=val_dataset,
compute_metrics=compute_metrics,
)
trainer.train()
results = trainer.evaluate(test_dataset)
print("Accuracy:", results["eval_accuracy"])
print("F1 Score:", results["eval_f1"])
preds = trainer.predict(test_dataset)
pred_labels = np.argmax(preds.predictions, axis=-1)
print(classification_report(test_dataset.labels, pred_labels, target_names=["Positif", "Negatif", "Netral"]))
Simpan & download model
import shutil
model.save_pretrained("./saved_model")
tokenizer.save_pretrained("./saved_model")
shutil.make_archive("saved_model", "zip", "./saved_model")
files.download("saved_model.zip")
classifier.weight/bias MISSING, ini normal — artinya layer klasifikasi baru diinisialisasi dan akan dilatih dari nol saat fine-tuning.
7. Membangun Aplikasi Web dengan Flask
Setelah model siap, saatnya membangun aplikasi web untuk memakainya. Untuk versi ini kita buat sederhana tanpa login dan tanpa database — fokus pada dua fitur utama: prediksi satu teks, dan prediksi massal lewat upload CSV.
7.1 Struktur Project
sentiment-simple/
├── app.py
├── ml_predict.py
├── utils.py
├── requirements.txt
├── saved_model/ (hasil extract dari Google Colab)
├── templates/
│ └── index.html
└── static/
├── style.css
└── script.js
7.2 Instalasi & Setup
python -m venv venv
# Windows
venv\Scripts\activate
# Linux/Mac
source venv/bin/activate
pip install -r requirements.txt
requirements.txt
Flask==2.3.0
transformers==4.35.0
torch==2.1.0
pandas==2.0.0
openpyxl==3.10.0
saved_model.zip selesai di-download dari Colab, extract dan letakkan folder saved_model/ di root project ini (sejajar dengan app.py).
7.3 Kode Backend Flask
utils.py — membersihkan teks sebelum diprediksi
import re
def preprocess_text(text):
text = str(text).lower()
text = re.sub(r'http\S+', '', text)
text = re.sub(r'@\w+', '', text)
text = re.sub(r'#\w+', '', text)
text = re.sub(r'[^\w\s]', ' ', text)
text = re.sub(r'\d+', '', text)
text = re.sub(r'\s+', ' ', text).strip()
return text
ml_predict.py — wrapper model IndoBERT
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
class SentimentPredictor:
def __init__(self, model_path):
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
self.tokenizer = AutoTokenizer.from_pretrained(model_path)
self.model = AutoModelForSequenceClassification.from_pretrained(model_path)
self.model.to(self.device)
self.model.eval()
self.id2label = {0: "Positif", 1: "Negatif", 2: "Netral"}
def predict(self, text):
encoding = self.tokenizer(
text, max_length=128, padding='max_length',
truncation=True, return_tensors='pt'
)
input_ids = encoding['input_ids'].to(self.device)
attention_mask = encoding['attention_mask'].to(self.device)
with torch.no_grad():
outputs = self.model(input_ids=input_ids, attention_mask=attention_mask)
probs = torch.softmax(outputs.logits, dim=1)
pred_class = torch.argmax(probs, dim=1).item()
confidence = probs[0][pred_class].item()
return {
"label": self.id2label[pred_class],
"confidence": round(confidence, 4)
}
_predictor = None
def get_predictor(model_path):
global _predictor
if _predictor is None:
_predictor = SentimentPredictor(model_path)
return _predictor
app.py — entry point Flask dengan 3 endpoint utama: prediksi teks, upload CSV, dan download hasil
import os
import io
import pandas as pd
from flask import Flask, render_template, request, jsonify, send_file
from ml_predict import get_predictor
from utils import preprocess_text
app = Flask(__name__)
app.config['MODEL_PATH'] = os.path.join(os.path.dirname(__file__), 'saved_model')
LAST_RESULT = None
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/predict', methods=['POST'])
def api_predict():
data = request.get_json(silent=True) or {}
text = (data.get('text') or '').strip()
if not text:
return jsonify({'error': 'Teks tidak boleh kosong'}), 400
try:
predictor = get_predictor(app.config['MODEL_PATH'])
clean_text = preprocess_text(text)
result = predictor.predict(clean_text)
return jsonify(result)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/upload', methods=['POST'])
def api_upload():
global LAST_RESULT
if 'file' not in request.files:
return jsonify({'error': 'Tidak ada file yang diupload'}), 400
file = request.files['file']
if file.filename == '' or not file.filename.lower().endswith('.csv'):
return jsonify({'error': 'File harus berformat .csv'}), 400
try:
df = pd.read_csv(file)
text_col = 'text' if 'text' in df.columns else df.columns[0]
df = df.dropna(subset=[text_col])
predictor = get_predictor(app.config['MODEL_PATH'])
labels, confidences = [], []
for text in df[text_col].astype(str):
clean_text = preprocess_text(text)
result = predictor.predict(clean_text)
labels.append(result['label'])
confidences.append(result['confidence'])
df['label'] = labels
df['confidence'] = confidences
summary = {
'total': int(len(df)),
'positif': int((df['label'] == 'Positif').sum()),
'negatif': int((df['label'] == 'Negatif').sum()),
'netral': int((df['label'] == 'Netral').sum()),
}
output = io.BytesIO()
df.to_excel(output, index=False, engine='openpyxl')
output.seek(0)
LAST_RESULT = output
preview = df.head(20)[[text_col, 'label', 'confidence']].rename(
columns={text_col: 'text'}
).to_dict(orient='records')
return jsonify({'success': True, **summary, 'preview': preview})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/download')
def api_download():
if LAST_RESULT is None:
return "Belum ada hasil prediksi untuk diunduh", 404
LAST_RESULT.seek(0)
return send_file(
LAST_RESULT,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
as_attachment=True,
download_name='hasil_prediksi_sentimen.xlsx'
)
if __name__ == '__main__':
app.run(debug=True)
7.4 Kode Frontend (Landing Page & Tools)
Halaman utama dibuat sebagai satu halaman (single page) yang berisi landing page singkat, form prediksi teks, dan form upload CSV. Menggunakan Bootstrap 5 untuk styling.
<!doctype html>
<html lang="id">
<head>
<meta charset="UTF-8">
<title>Analisis Sentimen Komentar YouTube - IndoBERT</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand fw-bold" href="/">Sentiment IndoBERT</a>
<a href="#tools" class="btn btn-primary btn-sm">Coba Sekarang</a>
</div>
</nav>
<header class="hero text-center text-white">
<div class="container py-5">
<h1 class="display-5 fw-bold">Analisis Sentimen Komentar YouTube</h1>
<p class="lead">Deteksi sentimen Positif, Negatif, atau Netral secara otomatis</p>
<a href="#tools" class="btn btn-light btn-lg mt-3">Mulai Analisis</a>
</div>
</header>
<section id="tools" class="container my-5">
<div class="card mb-5">
<div class="card-body">
<h4>Prediksi Satu Komentar</h4>
<textarea id="textInput" class="form-control mb-3" rows="4"></textarea>
<button id="btnPredict" class="btn btn-primary">Analisis Sentimen</button>
<div id="textResult" class="alert alert-info mt-4 d-none">
<p>Teks: <span id="resText"></span></p>
<p>Sentimen: <span id="resLabel" class="badge"></span></p>
<p>Confidence: <span id="resConfidence"></span></p>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<h4>Prediksi Massal (Upload CSV)</h4>
<div id="uploadBox" class="upload-box">
<p>Klik atau drag & drop file CSV di sini</p>
<input type="file" id="csvFile" accept=".csv" class="d-none">
</div>
<div id="batchResult" class="mt-4 d-none">
<a href="/api/download" class="btn btn-success">Download Hasil (Excel)</a>
</div>
</div>
</div>
</section>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>
script.js (untuk request AJAX ke endpoint /api/predict dan /api/upload) dan style.css ada di dalam file ZIP project yang bisa didownload di GitHub.
8. Menjalankan & Menguji Aplikasi
python app.py
Buka browser dan akses http://127.0.0.1:5000. Kamu akan melihat landing page, form prediksi teks, dan form upload CSV.
Cara pakai:
- Masukkan satu komentar di form teks, klik "Analisis Sentimen" untuk melihat hasilnya
- Atau upload file CSV berisi banyak komentar (kolom
text) untuk prediksi massal - Lihat ringkasan hasil (jumlah Positif/Negatif/Netral) dan preview tabel
- Klik "Download Hasil (Excel)" untuk mengunduh seluruh hasil prediksi
9. Penutup
Sampai di sini kita sudah berhasil membangun sistem analisis sentimen komentar YouTube secara end-to-end: mulai dari mengambil komentar lewat YouTube Data API, melabeli dataset mentah, fine-tuning model IndoBERT, sampai membungkusnya menjadi aplikasi web yang bisa dipakai untuk prediksi teks maupun prediksi massal lewat CSV.
Source code lengkap aplikasi web ini bisa dilihat dan di-clone langsung dari GitHub: github.com/irmanf11/sentiment-youtube-indobert, untuk dikembangkan lebih lanjut, misalnya menambahkan fitur word cloud, autentikasi, atau menyimpan hasil scraping otomatis ke database.

Posting Komentar