Analisis Sentimen Komentar YouTube dengan IndoBERT dan Flask (Tutorial Lengkap)

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.



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.

Tutorial ini mengasumsikan kamu sudah familiar dengan dasar Python. Untuk proses training model, kita akan memakai Google Colab supaya bisa memanfaatkan GPU gratis.

Alur besar yang akan kita lakukan:

  1. Membuat YouTube Data API key
  2. Scraping komentar YouTube menggunakan API tersebut (hasil: file Excel/CSV)
  3. Memberi label otomatis (Positif/Negatif/Netral) menggunakan kamus sentimen
  4. Fine-tuning model IndoBERT dengan dataset yang sudah berlabel
  5. 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:

  1. Buka Google Cloud Console dan login dengan akun Google kamu.
  2. Klik dropdown project di bagian atas, lalu klik "New Project". Beri nama bebas, misalnya youtube-sentiment, lalu klik Create.
  3. Setelah project dibuat dan terpilih, buka menu APIs & Services > Library (bisa dicari lewat search bar di atas).
  4. Cari "YouTube Data API v3", klik hasilnya, lalu klik tombol Enable.
  5. Setelah aktif, buka menu APIs & Services > Credentials.
  6. Klik Create Credentials > API key. Google akan langsung menghasilkan sebuah API key.
  7. Copy API key tersebut dan simpan baik-baik — ini yang akan dipakai di kode Colab.
Sebaiknya batasi API key ini supaya hanya bisa dipakai untuk YouTube Data API v3 (klik "Restrict Key" saat membuatnya). Jangan share API key ini secara publik, misalnya di GitHub, karena bisa disalahgunakan orang lain sampai kuota harianmu habis.
Kuota default YouTube Data API adalah 10.000 unit per hari. Mengambil komentar (commentThreads.list) memakan sekitar 1 unit per request/halaman, jadi cukup untuk mengambil ribuan komentar dari beberapa video sekaligus.

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")
File dataset_youtube_komentar.xlsx inilah yang selanjutnya dipakai sebagai dataset mentah untuk proses auto-labeling di langkah berikutnya.
Komentar yang dinonaktifkan pemiliknya (comments disabled) akan menyebabkan API mengembalikan error. Tambahkan blok 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")
Setelah proses ini selesai, cek beberapa sampel hasil labeling secara manual untuk memastikan hasilnya masuk akal sebelum dipakai untuk training.

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")
Ukuran folder model hasil fine-tuning IndoBERT-base biasanya sekitar 400-450MB. Ini normal karena model punya sekitar 110 juta parameter.
Jika muncul warning 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.

Malas copy-paste satu-satu? Source code lengkapnya sudah saya siapkan di GitHub: github.com/irmanf11/sentiment-youtube-indobert. Tinggal clone atau download ZIP-nya, lalu ikuti langkah instalasi di bawah.

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
Setelah 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>
Kode lengkap termasuk 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:

  1. Masukkan satu komentar di form teks, klik "Analisis Sentimen" untuk melihat hasilnya
  2. Atau upload file CSV berisi banyak komentar (kolom text) untuk prediksi massal
  3. Lihat ringkasan hasil (jumlah Positif/Negatif/Netral) dan preview tabel
  4. Klik "Download Hasil (Excel)" untuk mengunduh seluruh hasil prediksi
Aplikasi versi ini tidak memiliki autentikasi maupun database, sehingga tidak cocok untuk digunakan banyak pengguna sekaligus di production. Untuk kebutuhan multi-user (misalnya skripsi dengan role admin/user), perlu ditambahkan sistem login dan database seperti MySQL.

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.

Lebih lamaTerbaru

Posting Komentar