PerSent 0.1.6__tar.gz → 0.1.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of PerSent might be problematic. Click here for more details.
- {persent-0.1.6 → persent-0.1.7}/PKG-INFO +1 -1
- {persent-0.1.6 → persent-0.1.7}/PerSent/__init__.py +1 -1
- persent-0.1.7/PerSent/analyzer.py +117 -0
- {persent-0.1.6 → persent-0.1.7}/PerSent.egg-info/PKG-INFO +1 -1
- {persent-0.1.6 → persent-0.1.7}/setup.py +1 -1
- persent-0.1.6/PerSent/analyzer.py +0 -251
- {persent-0.1.6 → persent-0.1.7}/LICENSE.txt +0 -0
- {persent-0.1.6 → persent-0.1.7}/PerSent.egg-info/SOURCES.txt +0 -0
- {persent-0.1.6 → persent-0.1.7}/PerSent.egg-info/dependency_links.txt +0 -0
- {persent-0.1.6 → persent-0.1.7}/PerSent.egg-info/requires.txt +0 -0
- {persent-0.1.6 → persent-0.1.7}/PerSent.egg-info/top_level.txt +0 -0
- {persent-0.1.6 → persent-0.1.7}/README.md +0 -0
- {persent-0.1.6 → persent-0.1.7}/setup.cfg +0 -0
- {persent-0.1.6 → persent-0.1.7}/tests/test_basic.py +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from hazm import Normalizer, word_tokenize, Stemmer, stopwords_list
|
|
3
|
+
import re
|
|
4
|
+
from tqdm import tqdm
|
|
5
|
+
from gensim.models import Word2Vec
|
|
6
|
+
import numpy as np
|
|
7
|
+
from sklearn.linear_model import LogisticRegression
|
|
8
|
+
from sklearn.model_selection import train_test_split
|
|
9
|
+
import os
|
|
10
|
+
import joblib
|
|
11
|
+
from transformers import pipeline
|
|
12
|
+
import torch
|
|
13
|
+
|
|
14
|
+
class AdvancedPersianSentimentAnalyzer:
|
|
15
|
+
def __init__(self, model_dir='persian_sentiment/model'):
|
|
16
|
+
self.normalizer = Normalizer()
|
|
17
|
+
self.stemmer = Stemmer()
|
|
18
|
+
self.stopwords = set(stopwords_list())
|
|
19
|
+
self.model_dir = model_dir
|
|
20
|
+
self.vectorizer = None
|
|
21
|
+
self.classifier = None
|
|
22
|
+
|
|
23
|
+
# تنظیمات پیشرفته برای مدل احساسات
|
|
24
|
+
self.emotion_analyzer = pipeline(
|
|
25
|
+
"text-classification",
|
|
26
|
+
model="HooshvareLab/bert-fa-base-uncased-emotion",
|
|
27
|
+
tokenizer="HooshvareLab/bert-fa-base-uncased-emotion",
|
|
28
|
+
device=0 if torch.cuda.is_available() else -1,
|
|
29
|
+
return_all_scores=True,
|
|
30
|
+
function_to_apply='sigmoid' # اضافه کردن تابع فعالساز مناسب
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# پارامترهای بهینه شده
|
|
34
|
+
self.w2v_params = {
|
|
35
|
+
'vector_size': 300,
|
|
36
|
+
'window': 10,
|
|
37
|
+
'min_count': 2,
|
|
38
|
+
'workers': 4,
|
|
39
|
+
'sg': 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# لیست برچسبهای احساسات
|
|
43
|
+
self.emotion_labels = [
|
|
44
|
+
'Fear', 'Surprise', 'Anger',
|
|
45
|
+
'Contempt', 'Joy', 'Sadness', 'Disgust'
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
os.makedirs(self.model_dir, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
def _preprocess_emotion_text(self, text):
|
|
51
|
+
"""پیشپردازش تخصصی برای تحلیل احساسات"""
|
|
52
|
+
# نرمالسازی و حفظ ساختار احساسی
|
|
53
|
+
text = self.normalizer.normalize(text)
|
|
54
|
+
text = re.sub(r'([!?.]){2,}', r'\1', text) # حذف تکرار علائم نگارشی
|
|
55
|
+
text = re.sub(r'[۰-۹]', ' ', text) # حذف اعداد
|
|
56
|
+
return text.strip()
|
|
57
|
+
|
|
58
|
+
def _emotion_analysis(self, text):
|
|
59
|
+
"""تحلیل احساسات با مدیریت خطا و تنظیمات پیشرفته"""
|
|
60
|
+
try:
|
|
61
|
+
# پیشپردازش متن
|
|
62
|
+
processed_text = self._preprocess_emotion_text(text)
|
|
63
|
+
|
|
64
|
+
if len(processed_text) < 3: # بررسی متن کوتاه
|
|
65
|
+
raise ValueError("متن ورودی بسیار کوتاه است")
|
|
66
|
+
|
|
67
|
+
# تحلیل احساسات
|
|
68
|
+
results = self.emotion_analyzer(processed_text, top_k=None)[0]
|
|
69
|
+
|
|
70
|
+
# پردازش نتایج
|
|
71
|
+
emotions = {}
|
|
72
|
+
for item in results:
|
|
73
|
+
label = item['label']
|
|
74
|
+
score = item['score']
|
|
75
|
+
if label == 'ترس': emotions['Fear'] = score
|
|
76
|
+
elif label == 'تعجب': emotions['Surprise'] = score
|
|
77
|
+
elif label == 'خشم': emotions['Anger'] = score
|
|
78
|
+
elif label == 'تحقیر': emotions['Contempt'] = score
|
|
79
|
+
elif label == 'شادی': emotions['Joy'] = score
|
|
80
|
+
elif label == 'غم': emotions['Sadness'] = score
|
|
81
|
+
elif label == 'انزجار': emotions['Disgust'] = score
|
|
82
|
+
|
|
83
|
+
# نرمالسازی امتیازات
|
|
84
|
+
total = sum(emotions.values())
|
|
85
|
+
if total > 0:
|
|
86
|
+
for k in emotions:
|
|
87
|
+
emotions[k] = round(emotions[k]/total, 4)
|
|
88
|
+
|
|
89
|
+
# فیلتر کردن نتایج ضعیف
|
|
90
|
+
filtered = {k: v for k, v in emotions.items() if v >= 0.1}
|
|
91
|
+
|
|
92
|
+
return filtered if filtered else emotions
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
print(f"خطا در تحلیل احساسات: {str(e)}")
|
|
96
|
+
return {label: 0.0 for label in self.emotion_labels}
|
|
97
|
+
|
|
98
|
+
# بقیه متدها بدون تغییر (train, predict, save_model, load_model, ...)
|
|
99
|
+
|
|
100
|
+
# تست نمونه
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
analyzer = AdvancedPersianSentimentAnalyzer()
|
|
103
|
+
|
|
104
|
+
# تست با جملات نمونه
|
|
105
|
+
test_texts = [
|
|
106
|
+
"من واقعاً از این اتفاق خشمگین هستم! این غیرقابل قبوله",
|
|
107
|
+
"چه روز شگفتانگیزی بود، واقعاً خوشحالم",
|
|
108
|
+
"از دیدن این صحنه حالم به هم خورد، واقعاً منزجر کننده بود",
|
|
109
|
+
"میترسم این اتفاق دوباره تکرار بشه"
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
for text in test_texts:
|
|
113
|
+
print(f"\nمتن: {text}")
|
|
114
|
+
emotions = analyzer._emotion_analysis(text)
|
|
115
|
+
print("نتایج تحلیل احساسات:")
|
|
116
|
+
for emotion, score in emotions.items():
|
|
117
|
+
print(f"{emotion}: {score:.4f}")
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
import pandas as pd
|
|
2
|
-
from hazm import Normalizer, word_tokenize, Stemmer, stopwords_list
|
|
3
|
-
import re
|
|
4
|
-
from tqdm import tqdm
|
|
5
|
-
from gensim.models import Word2Vec
|
|
6
|
-
import numpy as np
|
|
7
|
-
from sklearn.linear_model import LogisticRegression
|
|
8
|
-
from sklearn.model_selection import train_test_split
|
|
9
|
-
import os
|
|
10
|
-
import joblib
|
|
11
|
-
from transformers import pipeline
|
|
12
|
-
import torch
|
|
13
|
-
from torch.nn.functional import softmax
|
|
14
|
-
|
|
15
|
-
class AdvancedPersianSentimentAnalyzer:
|
|
16
|
-
def __init__(self, model_dir='persian_sentiment/model'):
|
|
17
|
-
self.normalizer = Normalizer()
|
|
18
|
-
self.stemmer = Stemmer()
|
|
19
|
-
self.stopwords = set(stopwords_list())
|
|
20
|
-
self.model_dir = model_dir
|
|
21
|
-
self.vectorizer = None
|
|
22
|
-
self.classifier = None
|
|
23
|
-
|
|
24
|
-
# تنظیمات پیشرفته برای مدل احساسات
|
|
25
|
-
self.emotion_analyzer = pipeline(
|
|
26
|
-
"text-classification",
|
|
27
|
-
model="HooshvareLab/bert-fa-base-uncased-emotion",
|
|
28
|
-
tokenizer="HooshvareLab/bert-fa-base-uncased-emotion",
|
|
29
|
-
device=0 if torch.cuda.is_available() else -1,
|
|
30
|
-
return_all_scores=True
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
# پارامترهای بهینه شده
|
|
34
|
-
self.w2v_params = {
|
|
35
|
-
'vector_size': 300,
|
|
36
|
-
'window': 10,
|
|
37
|
-
'min_count': 2,
|
|
38
|
-
'workers': 4,
|
|
39
|
-
'sg': 1 # استفاده از skip-gram
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
# نگاشت برچسبهای احساسات
|
|
43
|
-
self.emotion_labels = {
|
|
44
|
-
'ترس': 'Fear',
|
|
45
|
-
'تعجب': 'Surprise',
|
|
46
|
-
'خشم': 'Anger',
|
|
47
|
-
'تحقیر': 'Contempt',
|
|
48
|
-
'شادی': 'Joy',
|
|
49
|
-
'غم': 'Sadness',
|
|
50
|
-
'انزجار': 'Disgust'
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
os.makedirs(self.model_dir, exist_ok=True)
|
|
54
|
-
|
|
55
|
-
def _preprocess_text(self, text, for_emotion=False):
|
|
56
|
-
"""پیشپردازش متن با قابلیت تنظیم برای تحلیل احساسات"""
|
|
57
|
-
text = self.normalizer.normalize(str(text))
|
|
58
|
-
|
|
59
|
-
if for_emotion:
|
|
60
|
-
# حفظ علائم نگارشی برای تحلیل احساسات
|
|
61
|
-
text = re.sub(r'[۰-۹\d]+', ' ', text)
|
|
62
|
-
else:
|
|
63
|
-
text = re.sub(r'[!()-\[\]{};:\'",؟<>./?@#$%^&*_~۰-۹\d]+', ' ', text)
|
|
64
|
-
|
|
65
|
-
text = re.sub(r'\s+', ' ', text).strip()
|
|
66
|
-
|
|
67
|
-
if not for_emotion:
|
|
68
|
-
tokens = word_tokenize(text)
|
|
69
|
-
return [
|
|
70
|
-
self.stemmer.stem(token)
|
|
71
|
-
for token in tokens
|
|
72
|
-
if token not in self.stopwords and len(token) > 1
|
|
73
|
-
]
|
|
74
|
-
return text
|
|
75
|
-
|
|
76
|
-
def _sentence_vector(self, sentence, model):
|
|
77
|
-
"""تبدیل جمله به بردار با میانگین وزنی"""
|
|
78
|
-
vectors = []
|
|
79
|
-
weights = []
|
|
80
|
-
|
|
81
|
-
for word in sentence:
|
|
82
|
-
try:
|
|
83
|
-
vectors.append(model.wv[word])
|
|
84
|
-
# وزندهی بر اساس فرکانس کلمه
|
|
85
|
-
weights.append(model.wv.get_vecattr(word, "count"))
|
|
86
|
-
except KeyError:
|
|
87
|
-
continue
|
|
88
|
-
|
|
89
|
-
if vectors:
|
|
90
|
-
weights = np.array(weights) / sum(weights)
|
|
91
|
-
return np.average(vectors, axis=0, weights=weights)
|
|
92
|
-
return np.zeros(self.w2v_params['vector_size'])
|
|
93
|
-
|
|
94
|
-
def _emotion_analysis(self, text):
|
|
95
|
-
"""تحلیل احساسات پیشرفته با مدل BERT"""
|
|
96
|
-
try:
|
|
97
|
-
# پیشپردازش متن برای تحلیل احساسات
|
|
98
|
-
processed_text = self._preprocess_text(text, for_emotion=True)
|
|
99
|
-
|
|
100
|
-
# تحلیل احساسات
|
|
101
|
-
results = self.emotion_analyzer(processed_text)[0]
|
|
102
|
-
|
|
103
|
-
# پردازش نتایج
|
|
104
|
-
emotions = {}
|
|
105
|
-
for item in results:
|
|
106
|
-
fa_label = item['label']
|
|
107
|
-
if fa_label in self.emotion_labels:
|
|
108
|
-
en_label = self.emotion_labels[fa_label]
|
|
109
|
-
score = round(float(item['score']), 4)
|
|
110
|
-
emotions[en_label] = score
|
|
111
|
-
|
|
112
|
-
# اگر همه مقادیر صفر بودند، قویترین احساس را برگردان
|
|
113
|
-
if all(v == 0 for v in emotions.values()) and results:
|
|
114
|
-
top_emotion = max(results, key=lambda x: x['score'])
|
|
115
|
-
fa_label = top_emotion['label']
|
|
116
|
-
if fa_label in self.emotion_labels:
|
|
117
|
-
en_label = self.emotion_labels[fa_label]
|
|
118
|
-
emotions[en_label] = round(float(top_emotion['score']), 4)
|
|
119
|
-
|
|
120
|
-
return emotions
|
|
121
|
-
|
|
122
|
-
except Exception as e:
|
|
123
|
-
print(f"خطا در تحلیل احساسات: {str(e)}")
|
|
124
|
-
return {label: 0.0 for label in self.emotion_labels.values()}
|
|
125
|
-
|
|
126
|
-
def train(self, train_csv, test_size=0.2):
|
|
127
|
-
"""آموزش پیشرفته مدل تحلیل توصیه"""
|
|
128
|
-
df = pd.read_csv(train_csv)
|
|
129
|
-
|
|
130
|
-
# تعادل کلاسها
|
|
131
|
-
class_counts = df['recommendation_status'].value_counts()
|
|
132
|
-
max_samples = class_counts.max()
|
|
133
|
-
balanced_df = pd.concat([
|
|
134
|
-
df[df['recommendation_status'] == cls].sample(max_samples, replace=True)
|
|
135
|
-
for cls in class_counts.index
|
|
136
|
-
])
|
|
137
|
-
|
|
138
|
-
balanced_df['tokens'] = balanced_df['body'].progress_apply(
|
|
139
|
-
lambda x: self._preprocess_text(x))
|
|
140
|
-
|
|
141
|
-
# آموزش مدل Word2Vec با پارامترهای بهینه
|
|
142
|
-
self.vectorizer = Word2Vec(
|
|
143
|
-
sentences=balanced_df['tokens'],
|
|
144
|
-
**self.w2v_params
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
# استخراج ویژگیهای پیشرفته
|
|
148
|
-
X = np.array([self._sentence_vector(s, self.vectorizer)
|
|
149
|
-
for s in tqdm(balanced_df['tokens'], desc="Vectorizing")])
|
|
150
|
-
y = balanced_df['recommendation_status'].map({
|
|
151
|
-
"no_idea": 2,
|
|
152
|
-
"recommended": 1,
|
|
153
|
-
"not_recommended": 0
|
|
154
|
-
}).values
|
|
155
|
-
|
|
156
|
-
# تقسیم دادهها با stratify
|
|
157
|
-
X_train, X_test, y_train, y_test = train_test_split(
|
|
158
|
-
X, y, test_size=test_size, stratify=y)
|
|
159
|
-
|
|
160
|
-
# مدل طبقهبندی با تنظیمات بهینه
|
|
161
|
-
self.classifier = LogisticRegression(
|
|
162
|
-
max_iter=2000,
|
|
163
|
-
class_weight='balanced',
|
|
164
|
-
C=0.5,
|
|
165
|
-
solver='saga'
|
|
166
|
-
)
|
|
167
|
-
self.classifier.fit(X_train, y_train)
|
|
168
|
-
|
|
169
|
-
# ذخیره مدل
|
|
170
|
-
self.save_model()
|
|
171
|
-
|
|
172
|
-
# ارزیابی دقیقتر
|
|
173
|
-
train_acc = self.classifier.score(X_train, y_train)
|
|
174
|
-
test_acc = self.classifier.score(X_test, y_test)
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
'train_accuracy': round(train_acc, 4),
|
|
178
|
-
'test_accuracy': round(test_acc, 4),
|
|
179
|
-
'model_params': self.w2v_params
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
def predict(self, text, mode=0, threshold=0.1):
|
|
183
|
-
"""
|
|
184
|
-
پیشبینی پیشرفته با قابلیت تنظیم آستانه
|
|
185
|
-
|
|
186
|
-
پارامترها:
|
|
187
|
-
text (str): متن ورودی
|
|
188
|
-
mode (int): حالت تحلیل (0=توصیه، 1=احساسات)
|
|
189
|
-
threshold (float): آستانه اطمینان برای تحلیل احساسات
|
|
190
|
-
|
|
191
|
-
بازگشتی:
|
|
192
|
-
dict یا str: نتایج تحلیل
|
|
193
|
-
"""
|
|
194
|
-
if mode == 1:
|
|
195
|
-
emotions = self._emotion_analysis(text)
|
|
196
|
-
# فیلتر کردن احساسات با احتمال پایین
|
|
197
|
-
return {
|
|
198
|
-
k: v for k, v in emotions.items()
|
|
199
|
-
if v >= threshold
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if not self.classifier or not self.vectorizer:
|
|
203
|
-
raise Exception("Model not trained! Call train() first.")
|
|
204
|
-
|
|
205
|
-
tokens = self._preprocess_text(text)
|
|
206
|
-
vector = self._sentence_vector(tokens, self.vectorizer)
|
|
207
|
-
proba = self.classifier.predict_proba([vector])[0]
|
|
208
|
-
|
|
209
|
-
# تصمیمگیری با در نظر گرفتن احتمال
|
|
210
|
-
if max(proba) < 0.6: # آستانه اطمینان
|
|
211
|
-
return "no_idea"
|
|
212
|
-
|
|
213
|
-
prediction = self.classifier.predict([vector])[0]
|
|
214
|
-
return {
|
|
215
|
-
0: "not_recommended",
|
|
216
|
-
1: "recommended",
|
|
217
|
-
2: "no_idea"
|
|
218
|
-
}[prediction]
|
|
219
|
-
|
|
220
|
-
def save_model(self):
|
|
221
|
-
"""ذخیره مدل با نسخهبندی"""
|
|
222
|
-
version = 1
|
|
223
|
-
while os.path.exists(os.path.join(self.model_dir, f'classifier_v{version}.joblib')):
|
|
224
|
-
version += 1
|
|
225
|
-
|
|
226
|
-
joblib.dump(
|
|
227
|
-
self.classifier,
|
|
228
|
-
os.path.join(self.model_dir, f'classifier_v{version}.joblib')
|
|
229
|
-
)
|
|
230
|
-
self.vectorizer.save(
|
|
231
|
-
os.path.join(self.model_dir, f'word2vec_v{version}.model')
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
def load_model(self, version=None):
|
|
235
|
-
"""بارگذاری مدل با قابلیت انتخاب نسخه"""
|
|
236
|
-
if version is None:
|
|
237
|
-
versions = [
|
|
238
|
-
int(f.split('_v')[-1].split('.')[0])
|
|
239
|
-
for f in os.listdir(self.model_dir)
|
|
240
|
-
if f.startswith('classifier_v')
|
|
241
|
-
]
|
|
242
|
-
version = max(versions) if versions else None
|
|
243
|
-
|
|
244
|
-
if version:
|
|
245
|
-
self.classifier = joblib.load(
|
|
246
|
-
os.path.join(self.model_dir, f'classifier_v{version}.joblib')
|
|
247
|
-
)
|
|
248
|
-
self.vectorizer = Word2Vec.load(
|
|
249
|
-
os.path.join(self.model_dir, f'word2vec_v{version}.model')
|
|
250
|
-
)
|
|
251
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|