PerSent 0.1.3__tar.gz → 0.1.4__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.3 → persent-0.1.4}/PKG-INFO +1 -1
- {persent-0.1.3 → persent-0.1.4}/PerSent/__init__.py +1 -1
- persent-0.1.4/PerSent/analyzer.py +222 -0
- {persent-0.1.3 → persent-0.1.4}/PerSent.egg-info/PKG-INFO +1 -1
- {persent-0.1.3 → persent-0.1.4}/setup.py +1 -1
- persent-0.1.3/PerSent/analyzer.py +0 -149
- {persent-0.1.3 → persent-0.1.4}/LICENSE.txt +0 -0
- {persent-0.1.3 → persent-0.1.4}/PerSent.egg-info/SOURCES.txt +0 -0
- {persent-0.1.3 → persent-0.1.4}/PerSent.egg-info/dependency_links.txt +0 -0
- {persent-0.1.3 → persent-0.1.4}/PerSent.egg-info/requires.txt +0 -0
- {persent-0.1.3 → persent-0.1.4}/PerSent.egg-info/top_level.txt +0 -0
- {persent-0.1.3 → persent-0.1.4}/README.md +0 -0
- {persent-0.1.3 → persent-0.1.4}/setup.cfg +0 -0
- {persent-0.1.3 → persent-0.1.4}/tests/test_basic.py +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
os.makedirs(self.model_dir, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
def _preprocess_text(self, text, for_emotion=False):
|
|
45
|
+
"""پیشپردازش متن با قابلیت تنظیم برای تحلیل احساسات"""
|
|
46
|
+
text = self.normalizer.normalize(str(text))
|
|
47
|
+
|
|
48
|
+
if for_emotion:
|
|
49
|
+
# حفظ علائم نگارشی برای تحلیل احساسات
|
|
50
|
+
text = re.sub(r'[۰-۹\d]+', ' ', text)
|
|
51
|
+
else:
|
|
52
|
+
text = re.sub(r'[!()-\[\]{};:\'",؟<>./?@#$%^&*_~۰-۹\d]+', ' ', text)
|
|
53
|
+
|
|
54
|
+
text = re.sub(r'\s+', ' ', text).strip()
|
|
55
|
+
|
|
56
|
+
if not for_emotion:
|
|
57
|
+
tokens = word_tokenize(text)
|
|
58
|
+
return [
|
|
59
|
+
self.stemmer.stem(token)
|
|
60
|
+
for token in tokens
|
|
61
|
+
if token not in self.stopwords and len(token) > 1
|
|
62
|
+
]
|
|
63
|
+
return text
|
|
64
|
+
|
|
65
|
+
def _sentence_vector(self, sentence, model):
|
|
66
|
+
"""تبدیل جمله به بردار با میانگین وزنی"""
|
|
67
|
+
vectors = []
|
|
68
|
+
weights = []
|
|
69
|
+
|
|
70
|
+
for word in sentence:
|
|
71
|
+
try:
|
|
72
|
+
vectors.append(model.wv[word])
|
|
73
|
+
# وزندهی بر اساس فرکانس کلمه
|
|
74
|
+
weights.append(model.wv.get_vecattr(word, "count"))
|
|
75
|
+
except KeyError:
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
if vectors:
|
|
79
|
+
weights = np.array(weights) / sum(weights)
|
|
80
|
+
return np.average(vectors, axis=0, weights=weights)
|
|
81
|
+
return np.zeros(self.w2v_params['vector_size'])
|
|
82
|
+
|
|
83
|
+
def _emotion_analysis(self, text):
|
|
84
|
+
"""تحلیل احساسات پیشرفته با مدیریت خطا"""
|
|
85
|
+
try:
|
|
86
|
+
processed_text = self._preprocess_text(text, for_emotion=True)
|
|
87
|
+
results = self.emotion_analyzer(processed_text)[0]
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
item['label']: round(float(item['score']), 4)
|
|
91
|
+
for item in results
|
|
92
|
+
if item['label'] in ['ترس', 'تعجب', 'خشم', 'تحقیر', 'شادی', 'غم', 'انزجار']
|
|
93
|
+
}
|
|
94
|
+
except Exception as e:
|
|
95
|
+
print(f"Error in emotion analysis: {str(e)}")
|
|
96
|
+
return {label: 0.0 for label in ['ترس', 'تعجب', 'خشم', 'تحقیر', 'شادی', 'غم', 'انزجار']}
|
|
97
|
+
|
|
98
|
+
def train(self, train_csv, test_size=0.2):
|
|
99
|
+
"""آموزش پیشرفته مدل تحلیل توصیه"""
|
|
100
|
+
df = pd.read_csv(train_csv)
|
|
101
|
+
|
|
102
|
+
# تعادل کلاسها
|
|
103
|
+
class_counts = df['recommendation_status'].value_counts()
|
|
104
|
+
max_samples = class_counts.max()
|
|
105
|
+
balanced_df = pd.concat([
|
|
106
|
+
df[df['recommendation_status'] == cls].sample(max_samples, replace=True)
|
|
107
|
+
for cls in class_counts.index
|
|
108
|
+
])
|
|
109
|
+
|
|
110
|
+
balanced_df['tokens'] = balanced_df['body'].progress_apply(
|
|
111
|
+
lambda x: self._preprocess_text(x))
|
|
112
|
+
|
|
113
|
+
# آموزش مدل Word2Vec با پارامترهای بهینه
|
|
114
|
+
self.vectorizer = Word2Vec(
|
|
115
|
+
sentences=balanced_df['tokens'],
|
|
116
|
+
**self.w2v_params
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# استخراج ویژگیهای پیشرفته
|
|
120
|
+
X = np.array([self._sentence_vector(s, self.vectorizer)
|
|
121
|
+
for s in tqdm(balanced_df['tokens'], desc="Vectorizing")])
|
|
122
|
+
y = balanced_df['recommendation_status'].map({
|
|
123
|
+
"no_idea": 2,
|
|
124
|
+
"recommended": 1,
|
|
125
|
+
"not_recommended": 0
|
|
126
|
+
}).values
|
|
127
|
+
|
|
128
|
+
# تقسیم دادهها با stratify
|
|
129
|
+
X_train, X_test, y_train, y_test = train_test_split(
|
|
130
|
+
X, y, test_size=test_size, stratify=y)
|
|
131
|
+
|
|
132
|
+
# مدل طبقهبندی با تنظیمات بهینه
|
|
133
|
+
self.classifier = LogisticRegression(
|
|
134
|
+
max_iter=2000,
|
|
135
|
+
class_weight='balanced',
|
|
136
|
+
C=0.5,
|
|
137
|
+
solver='saga'
|
|
138
|
+
)
|
|
139
|
+
self.classifier.fit(X_train, y_train)
|
|
140
|
+
|
|
141
|
+
# ذخیره مدل
|
|
142
|
+
self.save_model()
|
|
143
|
+
|
|
144
|
+
# ارزیابی دقیقتر
|
|
145
|
+
train_acc = self.classifier.score(X_train, y_train)
|
|
146
|
+
test_acc = self.classifier.score(X_test, y_test)
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
'train_accuracy': round(train_acc, 4),
|
|
150
|
+
'test_accuracy': round(test_acc, 4),
|
|
151
|
+
'model_params': self.w2v_params
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
def predict(self, text, mode=0, threshold=0.3):
|
|
155
|
+
"""
|
|
156
|
+
پیشبینی پیشرفته با قابلیت تنظیم آستانه
|
|
157
|
+
|
|
158
|
+
پارامترها:
|
|
159
|
+
text (str): متن ورودی
|
|
160
|
+
mode (int): حالت تحلیل (0=توصیه، 1=احساسات)
|
|
161
|
+
threshold (float): آستانه اطمینان برای تحلیل احساسات
|
|
162
|
+
|
|
163
|
+
بازگشتی:
|
|
164
|
+
dict یا str: نتایج تحلیل
|
|
165
|
+
"""
|
|
166
|
+
if mode == 1:
|
|
167
|
+
emotions = self._emotion_analysis(text)
|
|
168
|
+
# فیلتر کردن احساسات با احتمال پایین
|
|
169
|
+
return {
|
|
170
|
+
k: v for k, v in emotions.items()
|
|
171
|
+
if v >= threshold
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if not self.classifier or not self.vectorizer:
|
|
175
|
+
raise Exception("Model not trained! Call train() first.")
|
|
176
|
+
|
|
177
|
+
tokens = self._preprocess_text(text)
|
|
178
|
+
vector = self._sentence_vector(tokens, self.vectorizer)
|
|
179
|
+
proba = self.classifier.predict_proba([vector])[0]
|
|
180
|
+
|
|
181
|
+
# تصمیمگیری با در نظر گرفتن احتمال
|
|
182
|
+
if max(proba) < 0.6: # آستانه اطمینان
|
|
183
|
+
return "no_idea"
|
|
184
|
+
|
|
185
|
+
prediction = self.classifier.predict([vector])[0]
|
|
186
|
+
return {
|
|
187
|
+
0: "not_recommended",
|
|
188
|
+
1: "recommended",
|
|
189
|
+
2: "no_idea"
|
|
190
|
+
}[prediction]
|
|
191
|
+
|
|
192
|
+
def save_model(self):
|
|
193
|
+
"""ذخیره مدل با نسخهبندی"""
|
|
194
|
+
version = 1
|
|
195
|
+
while os.path.exists(os.path.join(self.model_dir, f'classifier_v{version}.joblib')):
|
|
196
|
+
version += 1
|
|
197
|
+
|
|
198
|
+
joblib.dump(
|
|
199
|
+
self.classifier,
|
|
200
|
+
os.path.join(self.model_dir, f'classifier_v{version}.joblib')
|
|
201
|
+
)
|
|
202
|
+
self.vectorizer.save(
|
|
203
|
+
os.path.join(self.model_dir, f'word2vec_v{version}.model')
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def load_model(self, version=None):
|
|
207
|
+
"""بارگذاری مدل با قابلیت انتخاب نسخه"""
|
|
208
|
+
if version is None:
|
|
209
|
+
versions = [
|
|
210
|
+
int(f.split('_v')[-1].split('.')[0])
|
|
211
|
+
for f in os.listdir(self.model_dir)
|
|
212
|
+
if f.startswith('classifier_v')
|
|
213
|
+
]
|
|
214
|
+
version = max(versions) if versions else None
|
|
215
|
+
|
|
216
|
+
if version:
|
|
217
|
+
self.classifier = joblib.load(
|
|
218
|
+
os.path.join(self.model_dir, f'classifier_v{version}.joblib')
|
|
219
|
+
)
|
|
220
|
+
self.vectorizer = Word2Vec.load(
|
|
221
|
+
os.path.join(self.model_dir, f'word2vec_v{version}.model')
|
|
222
|
+
)
|
|
@@ -1,149 +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 AutoModelForSequenceClassification, AutoTokenizer
|
|
12
|
-
import torch
|
|
13
|
-
from torch.nn.functional import softmax
|
|
14
|
-
|
|
15
|
-
class PersianSentimentAnalyzer:
|
|
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_tokenizer = AutoTokenizer.from_pretrained(
|
|
26
|
-
"HooshvareLab/bert-fa-base-uncased-emotion")
|
|
27
|
-
self.emotion_model = AutoModelForSequenceClassification.from_pretrained(
|
|
28
|
-
"HooshvareLab/bert-fa-base-uncased-emotion")
|
|
29
|
-
self.emotion_labels = ['ترس', 'تعجب', 'خشم', 'تحقیر', 'شادی', 'غم', 'انزجار']
|
|
30
|
-
|
|
31
|
-
os.makedirs(self.model_dir, exist_ok=True)
|
|
32
|
-
|
|
33
|
-
def _preprocess_text(self, text):
|
|
34
|
-
"""پیشپردازش متن فارسی"""
|
|
35
|
-
text = self.normalizer.normalize(str(text))
|
|
36
|
-
text = re.sub(r'[!()-\[\]{};:\'",؟<>./?@#$%^&*_~۰-۹\d]+', ' ', text)
|
|
37
|
-
text = re.sub(r'\s+', ' ', text).strip()
|
|
38
|
-
tokens = word_tokenize(text)
|
|
39
|
-
processed_tokens = [
|
|
40
|
-
self.stemmer.stem(token)
|
|
41
|
-
for token in tokens
|
|
42
|
-
if token not in self.stopwords and len(token) > 1
|
|
43
|
-
]
|
|
44
|
-
return processed_tokens
|
|
45
|
-
|
|
46
|
-
def _sentence_vector(self, sentence, model):
|
|
47
|
-
"""تبدیل جمله به بردار با میانگین بردارهای کلمات"""
|
|
48
|
-
vectors = []
|
|
49
|
-
for word in sentence:
|
|
50
|
-
try:
|
|
51
|
-
vectors.append(model.wv[word])
|
|
52
|
-
except KeyError:
|
|
53
|
-
vectors.append(np.zeros(100))
|
|
54
|
-
return np.mean(vectors, axis=0) if vectors else np.zeros(100)
|
|
55
|
-
|
|
56
|
-
def _emotion_analysis(self, text):
|
|
57
|
-
"""تحلیل احساسات با مدل BERT فارسی"""
|
|
58
|
-
inputs = self.emotion_tokenizer(
|
|
59
|
-
text,
|
|
60
|
-
return_tensors="pt",
|
|
61
|
-
truncation=True,
|
|
62
|
-
padding=True,
|
|
63
|
-
max_length=128
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
with torch.no_grad():
|
|
67
|
-
outputs = self.emotion_model(**inputs)
|
|
68
|
-
|
|
69
|
-
probs = softmax(outputs.logits, dim=1)
|
|
70
|
-
scores = probs.numpy()[0]
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
label: round(float(score), 4)
|
|
74
|
-
for label, score in zip(self.emotion_labels, scores)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
def train(self, train_csv, test_size=0.2, vector_size=100, window=5):
|
|
78
|
-
"""آموزش مدل تحلیل توصیه"""
|
|
79
|
-
df = pd.read_csv(train_csv)
|
|
80
|
-
df['tokens'] = df['body'].apply(self._preprocess_text)
|
|
81
|
-
|
|
82
|
-
# آموزش مدل Word2Vec
|
|
83
|
-
self.vectorizer = Word2Vec(
|
|
84
|
-
sentences=df['tokens'],
|
|
85
|
-
vector_size=vector_size,
|
|
86
|
-
window=window,
|
|
87
|
-
min_count=1,
|
|
88
|
-
workers=4
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
# تبدیل جملات به بردار
|
|
92
|
-
X = np.array([self._sentence_vector(s, self.vectorizer) for s in df['tokens']])
|
|
93
|
-
y = df['recommendation_status'].map({
|
|
94
|
-
"no_idea": 2,
|
|
95
|
-
"recommended": 1,
|
|
96
|
-
"not_recommended": 0
|
|
97
|
-
}).values
|
|
98
|
-
|
|
99
|
-
# تقسیم دادهها و آموزش مدل
|
|
100
|
-
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size)
|
|
101
|
-
self.classifier = LogisticRegression(max_iter=1000)
|
|
102
|
-
self.classifier.fit(X_train, y_train)
|
|
103
|
-
|
|
104
|
-
# ذخیره مدل
|
|
105
|
-
self.save_model()
|
|
106
|
-
accuracy = self.classifier.score(X_test, y_test)
|
|
107
|
-
return round(accuracy, 4)
|
|
108
|
-
|
|
109
|
-
def predict(self, text, mode=0):
|
|
110
|
-
"""
|
|
111
|
-
پیشبینی احساس متن
|
|
112
|
-
|
|
113
|
-
پارامترها:
|
|
114
|
-
text (str): متن ورودی
|
|
115
|
-
mode (int):
|
|
116
|
-
0 = تحلیل توصیه (پیشفرض)
|
|
117
|
-
1 = تحلیل احساسات با مدل BERT
|
|
118
|
-
|
|
119
|
-
بازگشتی:
|
|
120
|
-
dict یا str: نتایج تحلیل
|
|
121
|
-
"""
|
|
122
|
-
if mode == 0:
|
|
123
|
-
if not self.classifier or not self.vectorizer:
|
|
124
|
-
raise Exception("Recommendation model not trained! Call train() first.")
|
|
125
|
-
|
|
126
|
-
tokens = self._preprocess_text(text)
|
|
127
|
-
vector = self._sentence_vector(tokens, self.vectorizer)
|
|
128
|
-
prediction = self.classifier.predict([vector])[0]
|
|
129
|
-
return {
|
|
130
|
-
0: "not_recommended",
|
|
131
|
-
1: "recommended",
|
|
132
|
-
2: "no_idea"
|
|
133
|
-
}[prediction]
|
|
134
|
-
|
|
135
|
-
elif mode == 1:
|
|
136
|
-
return self._emotion_analysis(text)
|
|
137
|
-
|
|
138
|
-
else:
|
|
139
|
-
raise ValueError("Invalid mode. Use 0 (recommendation) or 1 (emotion)")
|
|
140
|
-
|
|
141
|
-
def save_model(self):
|
|
142
|
-
"""ذخیره مدل تحلیل توصیه"""
|
|
143
|
-
joblib.dump(self.classifier, os.path.join(self.model_dir, 'classifier.joblib'))
|
|
144
|
-
self.vectorizer.save(os.path.join(self.model_dir, 'word2vec.model'))
|
|
145
|
-
|
|
146
|
-
def load_model(self):
|
|
147
|
-
"""بارگذاری مدل تحلیل توصیه"""
|
|
148
|
-
self.classifier = joblib.load(os.path.join(self.model_dir, 'classifier.joblib'))
|
|
149
|
-
self.vectorizer = Word2Vec.load(os.path.join(self.model_dir, 'word2vec.model'))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|