File size: 6,475 Bytes
f7bde18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import pandas as pd
import numpy as np
import torch
from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
from sentence_transformers import SentenceTransformer
import faiss
import os
import sys

class SentimentRAG:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(SentimentRAG, cls).__new__(cls)
        return cls._instance

    def __init__(self, data_path="data/digikala_samples.csv", index_path=None):
        if hasattr(self, 'initialized') and self.initialized:
            return

        if index_path is None:
            if data_path == "data/digikala_samples.csv":
                index_path = "data/faiss_index.bin"
            else:
                index_path = data_path.replace(".csv", ".bin")

        print(f"Initializing SentimentRAG models (Online Optimized)...")
        torch.set_num_threads(2)

        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        hf_token = os.getenv("HUGGINGFACE_TOKEN")
        if hf_token == "": hf_token = None

        # 1. Sentiment Model
        self.sentiment_pipe = pipeline(
            "sentiment-analysis",
            model="nlptown/bert-base-multilingual-uncased-sentiment",
            device=-1 if self.device == "cpu" else 0,
            token=hf_token if hf_token else None,
            model_kwargs={"low_cpu_mem_usage": True} if self.device == "cpu" else {}
        )

        # 2. Embedding Model
        self.embed_model = SentenceTransformer(
            'sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2',
            use_auth_token=hf_token if hf_token else None
        )

        # 3. Reasoning Model
        self.gen_tokenizer = AutoTokenizer.from_pretrained("HooshvareLab/gpt2-fa-comment", token=hf_token if hf_token else None)
        self.gen_model = AutoModelForCausalLM.from_pretrained(
            "HooshvareLab/gpt2-fa-comment",
            token=hf_token if hf_token else None,
            low_cpu_mem_usage=True
        ).to(self.device)

        # Load Data & Index
        self._load_resources(data_path, index_path)

        self.initialized = True

    def _load_resources(self, data_path, index_path):
        self.df = None
        self.texts = []

        # Priority 1: Local file (if exists and is not LFS pointer)
        if os.path.exists(data_path):
            try:
                temp_df = pd.read_csv(data_path)
                if len(temp_df) > 0 and 'version https://git-lfs' in str(temp_df.columns[0]):
                    print("Found LFS pointer. Skipping local load.")
                else:
                    self.df = temp_df
                    self.texts = self.df['text'].tolist()
                    print(f"Loaded {len(self.texts)} samples from local file.")
            except Exception as e:
                print(f"Local CSV load failed: {e}")

        # Priority 2: Online streaming fallback
        if self.df is None:
            print("Fetching data from Hugging Face Hub (Streaming)...")
            try:
                from prepare_data import fetch_all_data
                self.df = fetch_all_data()
                if self.df is not None:
                    self.texts = self.df['text'].tolist()
                    print(f"Streamed {len(self.texts)} samples online.")
            except Exception as e:
                print(f"Online data streaming failed: {e}")

        if self.df is None:
             raise FileNotFoundError("System failed to load any data (Local/Online).")

        # FAISS Index Handling
        if os.path.exists(index_path):
            try:
                loaded_index = faiss.read_index(index_path)
                if loaded_index.ntotal == len(self.texts):
                    self.index = loaded_index
                    print("Loaded pre-generated FAISS index.")
                    return
            except Exception:
                pass

        print("Building FAISS index in memory...")
        self._build_index()

    def _build_index(self):
        embeddings = self.embed_model.encode(self.texts, show_progress_bar=False)
        self.index = faiss.IndexFlatL2(embeddings.shape[1])
        self.index.add(np.array(embeddings).astype('float32'))

    def get_sentiment(self, text):
        result = self.sentiment_pipe(text[:512])[0]
        score = int(result['label'].split()[0])
        return score, result['score']

    def retrieve_similar(self, text, k=2):
        k = min(k, len(self.texts))
        if k <= 0: return []
        query_vec = self.embed_model.encode([text])
        distances, indices = self.index.search(np.array(query_vec).astype('float32'), k)
        return [self.texts[i] for i in indices[0]]

    def generate_explanation(self, text, sentiment_score):
        similar_comments = self.retrieve_similar(text, k=2)
        context = " ".join([f"نمونه: {c[:60]}" for c in similar_comments])
        sentiment_label = "مثبت" if sentiment_score > 3 else "منفی" if sentiment_score < 3 else "خنثی"

        prompt = f"متن: {text[:100]}\nاحساس: {sentiment_label}\nشواهد: {context}\nدلیل فنی:"
        inputs = self.gen_tokenizer(prompt, return_tensors="pt", truncation=True, max_length=400).to(self.device)

        with torch.no_grad():
            outputs = self.gen_model.generate(
                **inputs,
                max_new_tokens=50,
                do_sample=True,
                top_p=0.9,
                temperature=0.7,
                pad_token_id=self.gen_tokenizer.eos_token_id
            )

        full_text = self.gen_tokenizer.decode(outputs[0], skip_special_tokens=True)
        if "دلیل فنی:" in full_text:
            explanation = full_text.split("دلیل فنی:")[-1].strip()
        else:
            explanation = "تحلیل بر اساس الگوهای متنی مشابه در پایگاه داده دیجی‌کالا انجام شده است."

        return explanation if len(explanation) > 10 else "این نظر به دلیل شباهت با نظرات ثبت شده قبلی و الگوهای کلامی شناسایی شده، دارای بار احساسی مشخص شده است."

if __name__ == "__main__":
    rag = SentimentRAG()
    test_text = "کیفیتش خوبه ولی قیمتش بالاست"
    score, conf = rag.get_sentiment(test_text)
    print(f"Sentiment: {score}, Confidence: {conf}")
    print(f"Reason: {rag.generate_explanation(test_text, score)}")