SicheateR99 commited on
Commit
eedc405
·
verified ·
1 Parent(s): ae673ab

Add Classification

Browse files
Files changed (1) hide show
  1. app.py +62 -49
app.py CHANGED
@@ -13,7 +13,7 @@ os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
13
  os.environ["TF_NUM_INTEROP_THREADS"] = "1"
14
  os.environ["TF_NUM_INTRAOP_THREADS"] = "1"
15
 
16
- app = FastAPI(title="NER API Service")
17
  app.add_middleware(
18
  CORSMiddleware,
19
  allow_origins=["*"],
@@ -22,6 +22,7 @@ app.add_middleware(
22
  allow_headers=["*"],
23
  )
24
 
 
25
  ner_model = None
26
  vocab = None
27
  MAX_SEQ_LEN = 64
@@ -30,39 +31,32 @@ UNK_TOKEN = "<UNK>"
30
  NUM_TOKEN = "<NUM>"
31
  PRICE_TOKEN = "<PRICE>"
32
 
 
 
 
 
33
  class TextRequest(BaseModel):
34
  text: str
35
 
36
- # =========================================================
37
- # TRIK BYPASS: BANGUN ARSITEKTUR MANUAL BERDASARKAN LOG
38
- # =========================================================
39
  def build_manual_model(vocab_size=7929, max_len=64, num_classes=8):
40
- # Nama setiap layer disamakan persis dengan log agar weights menempel sempurna
41
  inputs = tf.keras.Input(shape=(max_len,), dtype='int32', name='tokens')
42
-
43
  x = tf.keras.layers.Embedding(input_dim=vocab_size, output_dim=64, mask_zero=True, name='embedding')(inputs)
44
  x = tf.keras.layers.SpatialDropout1D(0.3, name='spatial_dropout1d')(x)
45
 
46
- # BiLSTM 1
47
  lstm1_f = tf.keras.layers.LSTM(128, return_sequences=True, dropout=0.4, name='forward_lstm')
48
  lstm1_b = tf.keras.layers.LSTM(128, return_sequences=True, go_backwards=True, dropout=0.4, name='backward_lstm')
49
  x = tf.keras.layers.Bidirectional(lstm1_f, backward_layer=lstm1_b, name='bilstm_1')(x)
50
 
51
- # BiLSTM 2
52
  lstm2_f = tf.keras.layers.LSTM(64, return_sequences=True, dropout=0.4, name='forward_lstm_1')
53
  lstm2_b = tf.keras.layers.LSTM(64, return_sequences=True, go_backwards=True, dropout=0.4, name='backward_lstm_1')
54
  x = tf.keras.layers.Bidirectional(lstm2_f, backward_layer=lstm2_b, name='bilstm_2')(x)
55
 
56
- # TimeDistributed Dense 1
57
  dense1 = tf.keras.layers.Dense(64, activation='relu', name='dense')
58
  x = tf.keras.layers.TimeDistributed(dense1, name='proj')(x)
59
  x = tf.keras.layers.Dropout(0.4, name='dropout')(x)
60
 
61
- # TimeDistributed Dense 2 (Logits)
62
  dense2 = tf.keras.layers.Dense(num_classes, activation='linear', name='dense_1')
63
  x = tf.keras.layers.TimeDistributed(dense2, name='logits')(x)
64
-
65
- # Output Softmax
66
  probs = tf.keras.layers.Activation('softmax', name='probabilities')(x)
67
 
68
  return tf.keras.Model(inputs=inputs, outputs=probs, name='BiLSTM_NER')
@@ -72,15 +66,18 @@ def get_ner_model():
72
  if ner_model is None:
73
  with open("vocab.pkl", "rb") as f:
74
  vocab = pickle.load(f)
75
-
76
- # 1. Buat kerangkanya
77
  ner_model = build_manual_model()
78
-
79
- # 2. HANYA isi bobotnya (Ini yang menyelamatkan kita dari error JSON!)
80
  ner_model.load_weights("receipt_ner_model.keras")
81
-
82
  return ner_model, vocab
83
 
 
 
 
 
 
 
 
 
84
  def normalize_token(token: str):
85
  token = token.strip()
86
  if re.fullmatch(r"\d{1,3}([.,]\d{3})*", token): return PRICE_TOKEN
@@ -93,47 +90,63 @@ def predict_ner(request: TextRequest):
93
  try:
94
  raw_text = request.text
95
  entities = {"MERCHANT": [], "ITEM": [], "TOTAL": []}
 
96
 
97
  if not raw_text.strip():
98
- return {"success": True, "entities": entities}
99
 
 
100
  model, vcb = get_ner_model()
101
-
102
  tokens = [t for line in raw_text.split("\n") for t in line.strip().split() if t]
103
- if not tokens:
104
- return {"success": True, "entities": entities}
105
-
106
- norm_tokens = [normalize_token(t) for t in tokens]
107
 
108
- # Hanya butuh array X, mask dihapus karena sudah otomatis ditangani layer Embedding
109
- X = np.zeros((1, MAX_SEQ_LEN), dtype=np.int32)
110
- length = min(len(norm_tokens), MAX_SEQ_LEN)
111
-
112
- for i in range(length):
113
- X[0, i] = vcb.get(norm_tokens[i], vcb.get(UNK_TOKEN, 0))
 
 
 
 
114
 
115
- # Inference super cepat
116
- probs = model(X, training=False).numpy()
117
- labels = [IDX2LABEL[int(np.argmax(probs[0, i]))] for i in range(length)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
- current_type = None
120
- span = []
121
- for tok, lbl in zip(tokens[:length], labels):
122
- if lbl.startswith("B-"):
123
- if current_type: entities[current_type].append(" ".join(span))
124
- current_type = lbl[2:]
125
- span = [tok]
126
- elif lbl.startswith("I-") and current_type == lbl[2:]:
127
- span.append(tok)
128
- else:
129
- if current_type: entities[current_type].append(" ".join(span))
130
- current_type = None
131
- span = []
132
- if current_type: entities[current_type].append(" ".join(span))
133
 
134
- return {"success": True, "entities": entities}
 
 
 
 
135
 
136
  except Exception as e:
137
  import traceback
138
- print(f"Error NER detail: {traceback.format_exc()}")
139
- return {"success": False, "error": str(e), "entities": {"MERCHANT": [], "ITEM": [], "TOTAL": []}}
 
13
  os.environ["TF_NUM_INTEROP_THREADS"] = "1"
14
  os.environ["TF_NUM_INTRAOP_THREADS"] = "1"
15
 
16
+ app = FastAPI(title="NER & Classification API Service")
17
  app.add_middleware(
18
  CORSMiddleware,
19
  allow_origins=["*"],
 
22
  allow_headers=["*"],
23
  )
24
 
25
+ # Global Variables NER
26
  ner_model = None
27
  vocab = None
28
  MAX_SEQ_LEN = 64
 
31
  NUM_TOKEN = "<NUM>"
32
  PRICE_TOKEN = "<PRICE>"
33
 
34
+ # Global Variables Classification
35
+ clf_model = None
36
+ clf_encoder = None
37
+
38
  class TextRequest(BaseModel):
39
  text: str
40
 
 
 
 
41
  def build_manual_model(vocab_size=7929, max_len=64, num_classes=8):
 
42
  inputs = tf.keras.Input(shape=(max_len,), dtype='int32', name='tokens')
 
43
  x = tf.keras.layers.Embedding(input_dim=vocab_size, output_dim=64, mask_zero=True, name='embedding')(inputs)
44
  x = tf.keras.layers.SpatialDropout1D(0.3, name='spatial_dropout1d')(x)
45
 
 
46
  lstm1_f = tf.keras.layers.LSTM(128, return_sequences=True, dropout=0.4, name='forward_lstm')
47
  lstm1_b = tf.keras.layers.LSTM(128, return_sequences=True, go_backwards=True, dropout=0.4, name='backward_lstm')
48
  x = tf.keras.layers.Bidirectional(lstm1_f, backward_layer=lstm1_b, name='bilstm_1')(x)
49
 
 
50
  lstm2_f = tf.keras.layers.LSTM(64, return_sequences=True, dropout=0.4, name='forward_lstm_1')
51
  lstm2_b = tf.keras.layers.LSTM(64, return_sequences=True, go_backwards=True, dropout=0.4, name='backward_lstm_1')
52
  x = tf.keras.layers.Bidirectional(lstm2_f, backward_layer=lstm2_b, name='bilstm_2')(x)
53
 
 
54
  dense1 = tf.keras.layers.Dense(64, activation='relu', name='dense')
55
  x = tf.keras.layers.TimeDistributed(dense1, name='proj')(x)
56
  x = tf.keras.layers.Dropout(0.4, name='dropout')(x)
57
 
 
58
  dense2 = tf.keras.layers.Dense(num_classes, activation='linear', name='dense_1')
59
  x = tf.keras.layers.TimeDistributed(dense2, name='logits')(x)
 
 
60
  probs = tf.keras.layers.Activation('softmax', name='probabilities')(x)
61
 
62
  return tf.keras.Model(inputs=inputs, outputs=probs, name='BiLSTM_NER')
 
66
  if ner_model is None:
67
  with open("vocab.pkl", "rb") as f:
68
  vocab = pickle.load(f)
 
 
69
  ner_model = build_manual_model()
 
 
70
  ner_model.load_weights("receipt_ner_model.keras")
 
71
  return ner_model, vocab
72
 
73
+ def get_clf_model():
74
+ global clf_model, clf_encoder
75
+ if clf_model is None:
76
+ with open('penerjemah_kategori.pkl', 'rb') as file:
77
+ clf_encoder = pickle.load(file)
78
+ clf_model = tf.keras.models.load_model('model_strukly.keras', compile=False)
79
+ return clf_model, clf_encoder
80
+
81
  def normalize_token(token: str):
82
  token = token.strip()
83
  if re.fullmatch(r"\d{1,3}([.,]\d{3})*", token): return PRICE_TOKEN
 
90
  try:
91
  raw_text = request.text
92
  entities = {"MERCHANT": [], "ITEM": [], "TOTAL": []}
93
+ kategori_struk = "Tidak Terklasifikasi"
94
 
95
  if not raw_text.strip():
96
+ return {"success": True, "entities": entities, "classes": kategori_struk}
97
 
98
+ # --- PROSES NER ---
99
  model, vcb = get_ner_model()
 
100
  tokens = [t for line in raw_text.split("\n") for t in line.strip().split() if t]
 
 
 
 
101
 
102
+ if tokens:
103
+ norm_tokens = [normalize_token(t) for t in tokens]
104
+ X = np.zeros((1, MAX_SEQ_LEN), dtype=np.int32)
105
+ length = min(len(norm_tokens), MAX_SEQ_LEN)
106
+
107
+ for i in range(length):
108
+ X[0, i] = vcb.get(norm_tokens[i], vcb.get(UNK_TOKEN, 0))
109
+
110
+ probs = model(X, training=False).numpy()
111
+ labels = [IDX2LABEL[int(np.argmax(probs[0, i]))] for i in range(length)]
112
 
113
+ current_type = None
114
+ span = []
115
+ for tok, lbl in zip(tokens[:length], labels):
116
+ if lbl.startswith("B-"):
117
+ if current_type: entities[current_type].append(" ".join(span))
118
+ current_type = lbl[2:]
119
+ span = [tok]
120
+ elif lbl.startswith("I-") and current_type == lbl[2:]:
121
+ span.append(tok)
122
+ else:
123
+ if current_type: entities[current_type].append(" ".join(span))
124
+ current_type = None
125
+ span = []
126
+ if current_type: entities[current_type].append(" ".join(span))
127
+
128
+ # --- PROSES KLASIFIKASI ---
129
+ items_list = entities.get("ITEM", [])
130
+ teks_untuk_klasifikasi = " ".join(items_list)
131
 
132
+ if teks_untuk_klasifikasi:
133
+ try:
134
+ c_model, c_encoder = get_clf_model()
135
+ input_data = np.array([teks_untuk_klasifikasi])
136
+
137
+ hasil_prediksi = c_model(input_data, training=False).numpy()
138
+ indeks_tertinggi = np.argmax(hasil_prediksi[0])
139
+ kategori_struk = c_encoder.inverse_transform([indeks_tertinggi])[0]
140
+ except Exception as e:
141
+ print(f"Error Klasifikasi: {e}")
 
 
 
 
142
 
143
+ return {
144
+ "success": True,
145
+ "entities": entities,
146
+ "classes": kategori_struk
147
+ }
148
 
149
  except Exception as e:
150
  import traceback
151
+ print(f"Error NER/CLF detail: {traceback.format_exc()}")
152
+ return {"success": False, "error": str(e), "entities": {"MERCHANT": [], "ITEM": [], "TOTAL": []}, "classes": "Error"}