File size: 8,436 Bytes
2b61832
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3cff72a
2b61832
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a336441
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
import os
import re
import joblib
import numpy as np
import pandas as pd
from sentence_transformers import SentenceTransformer
import lightgbm as lgb
import gradio as gr

stop_words = set([
    'the', 'a', 'an', 'and', 'or', 'but', 'is', 'are', 'am', 'to', 'for',
    'in', 'on', 'at', 'by', 'with', 'this', 'that', 'of', 'from', 'it',
    'its', 'they', 'them', 'their', 'was', 'were', 'be', 'been', 'being',
    'have', 'has', 'had', 'do', 'does', 'did', 'which', 'who', 'whom',
    'whose', 'where', 'when', 'how', 'all', 'any', 'both', 'each',
    'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not',
    'only', 'own', 'same', 'so', 'than', 'too', 'very', 's', 't', 'can',
    'will', 'just', 'don', 'should', 'now', 'i', 'me', 'my', 'myself',
    'we', 'our', 'ours', 'ourselves', 'you', 'your', 'yours', 'yourself',
    'yourselves', 'he', 'him', 'his', 'himself', 'she', 'her', 'hers',
    'herself', 'it', 'its', 'itself', 'they', 'them', 'their', 'theirs',
    'themselves'
])

top_brands_list = [
    'western digital', 'apple', 'samsung', 'dell', 'hp', 'lenovo', 'asus',
    'acer', 'sony', 'logitech', 'jbl', 'bose', 'tp-link', 'sandisk',
    'xiaomi', 'oppo', 'vivo', 'oneplus', 'google', 'amazon', 'nintendo',
    'playstation', 'xbox', 'msi', 'gigabyte'
]

premium_brands = [
    'apple', 'samsung', 'sony', 'dell', 'hp', 'bose', 'playstation',
    'xbox', 'nintendo'
]

# ============================ HELPER FUNCTIONS ============================
def clean_text_pro(text):
    """Làm sạch tên sản phẩm: lowercase, xoá ký tự đặc biệt, loại bỏ stopwords."""
    if not isinstance(text, str):
        return ""
    text = text.lower()
    text = re.sub(r'[^a-z0-9\s\.]', ' ', text)
    words = [w for w in text.split() if w not in stop_words and len(w) > 1]
    return " ".join(words)

def get_brand_tier(name):
    """Phân hạng thương hiệu: 2 = cao cấp, 1 = phổ biến, 0 = khác."""
    name_lower = str(name).lower()
    for brand in premium_brands:
        if re.search(r'\b' + re.escape(brand) + r'\b', name_lower):
            return 2
    for brand in top_brands_list:
        if re.search(r'\b' + re.escape(brand) + r'\b', name_lower):
            return 1
    return 0

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MODEL_DIR = os.path.join(BASE_DIR, '..', 'models')

model = joblib.load(os.path.join(MODEL_DIR, 'amazon_classifier.pkl'))
le = joblib.load(os.path.join(MODEL_DIR, 'label_encoder.pkl'))
pca = joblib.load(os.path.join(MODEL_DIR, 'pca_pro.pkl'))
svd = joblib.load(os.path.join(MODEL_DIR, 'svd_pro.pkl'))
tfidf = joblib.load(os.path.join(MODEL_DIR, 'tfidf_pro.pkl'))
scaler = joblib.load(os.path.join(MODEL_DIR, 'scaler_pro.pkl'))
numeric_cols = joblib.load(os.path.join(MODEL_DIR, 'numeric_cols.pkl'))

sent_model = SentenceTransformer('all-MiniLM-L6-v2')



def preprocess_and_predict(name, price, rating, reviews_count, bought_count, actual_price):
    """
    Nhận dữ liệu từ UI, biến đổi thành vector đặc trưng và dự đoán.
    Trả về dictionary {tên_lớp: xác_suất} cho Gradio hiển thị.
    """
    # 1. Văn bản (Text Features)
    text = clean_text_pro(str(name))
    emb = pca.transform(sent_model.encode([text]))         
    tfidf_vec = svd.transform(tfidf.transform([text]))      

    # 2. Biến số (Numerical Features)
    feat = {
        'price_log': np.log1p(price),
        'rating_clean': float(rating) if rating else 4.2,
        'reviews_log': np.log1p(float(reviews_count)) if reviews_count else 0,
        'bought_count_log': np.log1p(bought_count),
        'discount_pct': (actual_price - price) / actual_price if actual_price > price else 0,
        'social_proof': np.log1p(float(rating) * float(reviews_count)) if rating and reviews_count else 0,
        'word_count': len(str(name).split()),
        'brand_tier': get_brand_tier(name),
        'is_best_seller_flag': 0, 
        'has_coupon': 0           
    }

    # 3. Scale các đặc trưng liên tục (Numerical scaling)
    scaler_input_cols = [
        'price_log', 'rating_clean', 'reviews_log', 'bought_count_log',
        'discount_pct', 'social_proof', 'word_count', 'brand_tier',
        'is_best_seller_flag', 'has_coupon'
    ]
    df_input = pd.DataFrame([[feat.get(c, 0) for c in scaler_input_cols]], columns=scaler_input_cols)
    scaled_input = pd.DataFrame(scaler.transform(df_input), columns=scaler_input_cols)
    final_num = scaled_input[numeric_cols].values # Chỉ giữ cột có sau VIF

    X = np.hstack([final_num, emb, tfidf_vec])

    # 5. Dự đoán
    proba = model.predict_proba(X)[0]
    result = {le.classes_[i]: float(proba[i]) for i in range(len(le.classes_))}
    sorted_result = dict(sorted(result.items(), key=lambda item: item[1], reverse=True))
    max_conf = max(proba)

    if max_conf < 0.5:
        top_result = dict(list(sorted_result.items())[:1])
        warning_msg = f"**Cảnh báo**: Mức độ tin cậy cao nhất chỉ đạt **{max_conf*100:.1f}%**. Sản phẩm này có thông tin không rõ ràng hoặc có thể thuộc danh mục khác."
    else:
        top_result = dict(list(sorted_result.items())[:3])
        warning_msg = f"Dự đoán thành công với mức độ tin cậy (**{max_conf*100:.1f}%**)."

    return top_result, warning_msg


custom_theme = gr.themes.Soft(
    primary_hue="blue",
    secondary_hue="indigo",
    neutral_hue="slate",
    font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"]
)

with gr.Blocks(title="Amazon Product Classifier") as demo:
    gr.Markdown(
        """
        # Ứng dụng Phân loại Sản phẩm Amazon (Amazon Product Classifier)
        **Mô hình phân loại Machine Learning** dự đoán danh mục sản phẩm trên Amazon thông qua **LightGBM** và **Sentence-BERT**.
        Mô hình phân loại thành 1 trong 10 danh mục khác nhau.
        """
    )
    
    with gr.Row():
        # Cột Input
        with gr.Column(scale=2):
            gr.Markdown("###  Nhập thông tin Sản phẩm")
            name = gr.Textbox(
                label="Tên sản phẩm",
                placeholder="VD: Apple iPhone 15 Pro Max 256GB Titanium, Blue",
                lines=2
            )
            
            with gr.Row():
                price = gr.Number(label="Giá khuyến mãi ($)", value=0.0, precision=2)
                actual_price = gr.Number(label="Giá gốc ($)", value=0.0, precision=2)
            
            with gr.Row():
                rating = gr.Slider(label="Điểm đánh giá (Rating)", minimum=1.0, maximum=5.0, step=0.1, value=4.5)
                reviews_count = gr.Number(label="Số lượt đánh giá (Reviews)", value=100, precision=0)
                bought_count = gr.Number(label="Lượt mua tháng trước", value=0, precision=0)
            
            predict_btn = gr.Button(" Dự đoán Danh mục", variant="primary", size="lg")
            
        # Cột Output
        with gr.Column(scale=1):
            gr.Markdown("###  Kết quả Dự đoán")
            output_label = gr.Label(label="Top 3 danh mục phù hợp nhất", num_top_classes=3)
            warning_text = gr.Markdown(value="", visible=True)
            
    # Sự kiện khi bấm nút Predict
    predict_btn.click(
        fn=preprocess_and_predict,
        inputs=[name, price, rating, reviews_count, bought_count, actual_price],
        outputs=[output_label, warning_text]
    )
    
    # Bảng Ví dụ để test nhanh
    gr.Markdown("###  Ví dụ (Click để điền tự động)")
    gr.Examples(
        examples=[
            ["Apple iPhone 15 Pro Max 256GB Titanium", 1199, 4.8, 5000, 1000, 1299],
            ["Samsung 32-inch Odyssey G5 Gaming Monitor", 300, 4.5, 1200, 500, 350],
            ["Logitech MX Master 3S Wireless Mouse", 99, 4.9, 3000, 800, 99],
            ["PlayStation 5 Console (PS5)", 499, 4.9, 55000, 5000, 499],
            ["Premium Yoga Mat for Exercise", 20, 4.0, 150, 50, 30]
        ],
        inputs=[name, price, rating, reviews_count, bought_count, actual_price],
        label="Dữ liệu mẫu"
    )

    gr.Markdown(
        """
        ---
        **Tác giả**: Phương Thảo | **Email**: phtee2412@gmail.com
        """
    )

if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", server_port=7860, theme=custom_theme, css="footer {visibility: hidden}")