classifier2may / app.py
Engin34's picture
update app.py 16may
ce1bf09 verified
Raw
History Blame Contribute Delete
21.5 kB
import os, re, time, math, unicodedata, logging
from contextlib import asynccontextmanager
from collections import Counter
from dataclasses import dataclass, field
from typing import Dict, List, Optional
import numpy as np
import torch
import torch.nn as nn
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from transformers import AutoTokenizer, AutoModel, AutoModelForSeq2SeqLM
from keybert import KeyBERT
from huggingface_hub import hf_hub_download, snapshot_download
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("liftup")
HF_USERNAME = os.getenv("HF_USERNAME", "Engin34")
HF_TOKEN = os.getenv("HF_TOKEN", "")
GEMINI_KEY = os.getenv("GEMINI_API_KEY", "")
bert_model = None
bert_tok = None
kw_model = None
generator = None
clf = None
TOP_KEYWORDS = None
_ext_model = None
_EXT_OK = False
ALL_CATS = []
# ── Taksonomi parser ─────────────────────────────────────────────────
def _temizle(k):
k = k.replace('\u200b', '').replace('\ufeff', '')
k = unicodedata.normalize('NFKC', k)
return re.sub(r'\s+', ' ', k).strip().lower()
def _parantez_ayir(k):
m = re.match(r'^(.+?)\s*\((.+?)\)\s*$', k)
if not m:
return [k]
ana, ic = m.group(1).strip(), m.group(2).strip()
if any(a in ic.lower() for a in ['baglam', 'proses', 'analiz', 'anahtarlari']):
return [ana]
return [ana] + [p.strip() for p in ic.split('/') if p.strip()]
def _virgul_ayir(metin):
sonuc, buf, d = [], [], 0
for c in metin:
if c == '(':
d += 1
buf.append(c)
elif c == ')':
d -= 1
buf.append(c)
elif c == ',' and d == 0:
sonuc.append(''.join(buf))
buf = []
else:
buf.append(c)
if buf:
sonuc.append(''.join(buf))
return sonuc
def parse_taksonomi(icerik: str) -> Dict:
icerik = icerik.replace('\u200b', '')
matches = list(re.finditer(r'^\s*(\d+)\)\s+(.+?)\s*$', icerik, re.MULTILINE))
tax = {}
for i, m in enumerate(matches):
kat = m.group(2).strip()
govde = icerik[m.end():(matches[i+1].start() if i+1 < len(matches) else len(icerik))].strip()
pm = re.search(r'\((.+)\)', govde, re.DOTALL)
if not pm:
continue
kw_set = set()
for parca in _virgul_ayir(pm.group(1)):
for alt in _parantez_ayir(parca.strip()):
for k in re.split(r'[/]', alt):
temiz = _temizle(k)
if len(temiz) >= 2:
kw_set.add(temiz)
tax[kat] = {'keywords': kw_set}
return tax
# ── Taksonomi genisletme sozlugu ─────────────────────────────────────
_EKSTRA_KW = {
'Yapay Zeka & Veri Bilimi': {
'derin ogrenme', 'sinir agi', 'transformer', 'lstm', 'gru',
'makine ogrenmesi', 'yapay sinir', 'classification', 'regresyon',
'veri madenciligi', 'takviyeli ogrenme', 'xgboost',
},
'Yapilar, Mukavemet & Analiz': {
'sonlu elemanlar', 'fea', 'burkulma', 'yorulma', 'hasar',
'gerilme', 'deformasyon', 'mod analizi', 'titresim', 'nastran',
'abaqus', 'ansys', 'mukavemet',
},
'Goruntu Isleme & Algilama': {
'nesne tespiti', 'segmentasyon', 'lidar', 'radar', 'yolo',
'resnet', 'konvolusyon', 'termal', 'infrared', 'optik akis',
},
'Otonom Sistemler & Robotik': {
'otonom', 'iha', 'uav', 'suru', 'navigasyon', 'haritalama',
'slam', 'path planning', 'insansiz',
},
'Ucus Mekanigi': {
'stabilite', 'trim', 'manevra', 'irtifa', 'gudum',
'ucus yorungesi', 'agirlik merkezi', 'balistik',
'ucus dinamigi', 'manevrabilite', 'yukleme', 'flaperon',
'elevator', 'rudder', 'aileron', 'moment', 'kaldirma',
'surukleme', 'aoa', 'hucum acisi', 'dalis', 'tirmanma',
},
'Kontrol & Aviyonik': {
'pid', 'kontrol sistemi', 'geri besleme', 'servo', 'gyro',
'ivmeolcer', 'imu', 'kalman', 'fpga', 'gomulu',
'mikrodenetleyici', 'aviyonik', 'ucus bilgisayari',
'otopilot', 'fms', 'actuator', 'sensor fuzyonu',
},
'Eklemeli Imalat': {
'3d baski', 'slm', 'fdm', 'sls', 'dmls', 'ebm',
'topoloji optimizasyonu', 'kafes yapi', 'metal baski',
'polimer', 'eklemeli', 'additive', 'katman katman',
'tarama stratejisi', 'goz enek', 'yogunluk',
},
'Yazilim Gelistirme': {
'gomulu yazilim', 'rtos', 'misra', 'do-178', 'arinc',
'mil-std', 'yazilim dogrulama', 'test otomasyonu',
'model tabanli tasarim', 'simulink', 'model based',
},
}
# ── Veri siniflari ───────────────────────────────────────────────────
@dataclass
class KategoriSkoru:
kategori: str
final_skor: float
keyword_skor: float
semantic_skor: float
eslesmeler: list = field(default_factory=list)
# ── Hibrit siniflandirici ────────────────────────────────────────────
class HibritSiniflandirici:
def __init__(self, taxonomy, embedder,
keyword_weight=0.35, semantic_weight=0.65, title_boost=2.0):
self.taxonomy = self._expand(
{c: {'keywords': {str(k).lower().strip()
for k in d.get('keywords', set()) if str(k).strip()}}
for c, d in taxonomy.items()}
)
self.kw_w = keyword_weight
self.sem_w = semantic_weight
self.title_boost = title_boost
self.embedder = embedder
self.idf = self._idf()
self.centroids = self._centroids()
log.info("Hibrit siniflandirici hazir: %d kategori", len(self.taxonomy))
def _expand(self, tax):
import copy
t = copy.deepcopy(tax)
for cat, kws in _EKSTRA_KW.items():
if cat in t:
t[cat]['keywords'] |= kws
return t
def _idf(self):
cnt = Counter()
N = len(self.taxonomy)
for d in self.taxonomy.values():
for k in d['keywords']:
cnt[k] += 1
return {k: math.log((N + 1) / (v + 1)) + 1 for k, v in cnt.items()}
def _centroids(self):
c = {}
for cat, d in self.taxonomy.items():
kws = list(d['keywords'])[:30]
if not kws:
c[cat] = None
continue
embs = self.embedder.encode(kws, show_progress_bar=False, convert_to_numpy=True)
v = np.mean(embs, axis=0)
n = np.linalg.norm(v)
c[cat] = v / n if n > 0 else v
return c
def _kw_score(self, extracted, title_kws=None):
ext = [k.lower().strip() for k in extracted if k and str(k).strip()]
max_idf = max(self.idf.values(), default=1.0)
results = {}
for cat, d in self.taxonomy.items():
cat_kws = d['keywords']
score, eslm = 0.0, []
for kw in ext:
boost = self.title_boost if (title_kws and kw in title_kws) else 1.0
idf_w = self.idf.get(kw, 1.0)
if kw in cat_kws:
p = 2.0 * boost * idf_w
score += p
eslm.append(kw)
elif len(kw) >= 4 and any(kw in ck or ck in kw for ck in cat_kws):
p = 1.0 * boost * idf_w
score += p
eslm.append(kw)
max_p = max(len(ext) * 2.0 * max_idf, 1e-6)
results[cat] = (min(score / max_p, 1.0), eslm)
return results
def _sem_score(self, extracted, text=None):
parts = []
if text and str(text).strip():
parts.append(str(text).strip())
if extracted:
parts.append(' '.join(str(k) for k in extracted))
if not parts:
return {c: 0.0 for c in self.taxonomy}
emb = self.embedder.encode(
[' | '.join(parts)], show_progress_bar=False, convert_to_numpy=True
)[0]
n = np.linalg.norm(emb)
if n > 0:
emb = emb / n
return {
cat: max(0.0, min(1.0, (float(np.dot(emb, cn)) + 1.0) / 2.0))
if cn is not None else 0.0
for cat, cn in self.centroids.items()
}
def classify(self, keywords, text=None, title=None, top_k=3):
title_kws = {w.lower() for w in str(title).split()} if title else None
kw_r = self._kw_score(keywords, title_kws)
sem_s = self._sem_score(keywords, text)
scores = {}
for c in self.taxonomy:
kwn, esl = kw_r[c]
f = self.kw_w * kwn + self.sem_w * sem_s[c]
scores[c] = KategoriSkoru(c, f, kwn, sem_s[c], esl)
srt = sorted(scores.values(), key=lambda x: x.final_skor, reverse=True)
return {
'prediction': srt[0].kategori,
'confidence': srt[0].final_skor,
'top_k': srt[:top_k],
'all_scores': scores,
}
# ── BERT modeli ──────────────────────────────────────────────────────
class LiftUpBertModel(nn.Module):
def __init__(self, num_labels=128):
super().__init__()
self.bert = AutoModel.from_pretrained('dbmdz/bert-base-turkish-cased')
self.dropout = nn.Dropout(0.3)
self.classifier = nn.Linear(768, num_labels)
def forward(self, input_ids, attention_mask):
out = self.bert(input_ids=input_ids, attention_mask=attention_mask)
return self.classifier(self.dropout(out.last_hidden_state[:, 0]))
# ── Post-processor ───────────────────────────────────────────────────
class SoftPostProcessor:
BLACKLIST = {'kombinatur', 'hesonomik', 'modulasyonlari', 'difuzorlue', 'optimizasyonlarini'}
ACRONYMS = {'CFD', 'FEA', 'CAD', 'ROS', 'CNN', 'AI', 'ML', 'DL', 'IoT', 'GPU', 'SSD', 'API'}
def fix_case(self, kw):
parts = []
for w in kw.split():
if w.upper() in self.ACRONYMS:
parts.append(w.upper())
elif not parts:
parts.append(w.capitalize())
else:
parts.append(w.lower())
return ' '.join(parts)
def ok(self, kw):
return (
kw.lower() not in self.BLACKLIST
and 3 <= len(kw) <= 80
and not re.search(r'[^a-zA-Z\xc7\xe7\u011e\u011f\u0130\u0131\xd6\xf6\u015e\u015f\xdc\xfc\s\-]', kw)
)
def process(self, kws, min_kw=3):
out, seen = [], set()
for k in kws:
if not self.ok(k):
continue
f = self.fix_case(k)
if f.lower() not in seen:
out.append(f)
seen.add(f.lower())
return out[:8] if out else kws[:min_kw]
# ── Ek destek modulu ─────────────────────────────────────────────────
def _init_ext(api_key, cat_list):
global _ext_model, _EXT_OK
if not api_key:
return
try:
import google.generativeai as genai
genai.configure(api_key=api_key)
_ext_model = genai.GenerativeModel('gemini-2.5-flash')
_ext_model.generate_content('Hi')
_EXT_OK = True
log.info('Ek destek modulu hazir')
except Exception as e:
_EXT_OK = False
log.warning('Ek destek modulu: %s', e)
def _ext_keywords(title, abstract):
if not _EXT_OK or _ext_model is None:
return []
try:
prompt = (
f'Havacilk ve savunma sanayi alaninda uzman bir arastirmacisin.\n'
f'Asagidaki projenin teknik anahtar kelimelerini belirle.\n\n'
f'PROJE BASLIGI: {title}\n'
f'PROJE OZETI: {abstract[:500]}\n\n'
f'KURALLAR:\n'
f'- Yalnizca bu projede gecen teknik terimler, yontemler, malzemeler ve teknolojiler\n'
f'- Genel kelimeler degil (analiz, sistem, gelistirme gibi) -- ozgun teknik terimler\n'
f'- Turkce yaz, kisaltmalar buyuk harf (CFD, ROS, CFRP, LSTM gibi)\n'
f'- VirgΓΌlle ayir, en fazla 6 kelime\n\n'
f'YANIT (sadece virgΓΌlle ayrilmis kelimeler):'
)
raw = _ext_model.generate_content(prompt).text.strip()
lines = [l.strip() for l in raw.split('\n') if l.strip()]
raw = lines[-1] if lines else raw
return [k.strip().strip('.-') for k in raw.split(',') if k.strip()][:6]
except Exception:
return []
def _ext_classify(title, abstract, cat_list):
if not _EXT_OK or _ext_model is None:
return None
cat_str = '\n'.join(f'{i+1}. {c}' for i, c in enumerate(cat_list))
prompt = (
f'Sen bir havacilk ve savunma sanayi Ar-Ge projesi siniflandirma uzmanissin.\n'
f'Asagidaki projeyi, verilen 15 kategoriden TAM OLARAK BIRINE ata.\n\n'
f'PROJE BASLIGI: {title}\n'
f'PROJE OZETI: {abstract[:600]}\n\n'
f'KATEGORILER:\n{cat_str}\n\n'
f'KARAR KURALLARI:\n'
f'- Projenin ana konusuna ve kullandigi yonteme odaklan\n'
f'- Basliktaki anahtar kelimeler genellikle en guclu ipucudur\n'
f'- Birden fazla konu varsa, en baskin olani sec\n'
f'- Sadece listedeki kategorilerden birini sec\n\n'
f'YANIT FORMATI (baska hicbir sey yazma):\n'
f'SAYI. Kategori Adi\n\n'
f'Ornek: 8. Kompozit Yapilar'
)
try:
raw = _ext_model.generate_content(prompt).text.strip()
m = re.search(r'(\d+)\.', raw)
if m:
num = int(m.group(1))
if 1 <= num <= len(cat_list):
return cat_list[num - 1]
raw_lower = raw.lower()
for c in cat_list:
if c.lower() in raw_lower:
return c
except Exception:
pass
return None
def _combine_scores(hybrid_result, extra_cat, boost=0.08):
if not extra_cat:
return hybrid_result
scores = hybrid_result['all_scores']
if extra_cat in scores:
old = scores[extra_cat]
scores[extra_cat] = KategoriSkoru(
kategori = old.kategori,
final_skor = min(old.final_skor + boost, 1.0),
keyword_skor = old.keyword_skor,
semantic_skor = old.semantic_skor,
eslesmeler = old.eslesmeler,
)
srt = sorted(scores.values(), key=lambda x: x.final_skor, reverse=True)
return {
'prediction': srt[0].kategori,
'confidence': srt[0].final_skor,
'top_k': srt[:3],
'all_scores': scores,
}
# ── Model yukleme ────────────────────────────────────────────────────
def load_models():
global bert_model, bert_tok, kw_model, generator, clf, TOP_KEYWORDS, ALL_CATS
auth = {'token': HF_TOKEN} if HF_TOKEN else {}
tax_path = hf_hub_download(
repo_id=f'{HF_USERNAME}/liftup-bert', filename='taksonomi.txt', **auth
)
with open(tax_path, encoding='utf-8') as f:
taxonomy = parse_taksonomi(f.read())
ALL_CATS = list(taxonomy.keys())
ckpt_path = hf_hub_download(
repo_id=f'{HF_USERNAME}/liftup-bert', filename='checkpoint.pth', **auth
)
ckpt = torch.load(ckpt_path, map_location='cpu')
TOP_KEYWORDS = ckpt['TOP_KEYWORDS']
bert_path = hf_hub_download(
repo_id=f'{HF_USERNAME}/liftup-bert', filename='best_bert_model.pth', **auth
)
bert_tok = AutoTokenizer.from_pretrained('dbmdz/bert-base-turkish-cased')
model = LiftUpBertModel(len(TOP_KEYWORDS))
model.load_state_dict(torch.load(bert_path, map_location='cpu'))
model.eval()
bert_model = model
log.info('BERT hazir')
kw_model = KeyBERT(model='sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')
embedder = kw_model.model.embedding_model
log.info('KeyBERT hazir')
byt5_dir = snapshot_download(repo_id=f'{HF_USERNAME}/liftup-byt5', **auth)
byt5_tok = AutoTokenizer.from_pretrained('google/byt5-small')
byt5_mdl = AutoModelForSeq2SeqLM.from_pretrained(byt5_dir)
byt5_mdl.eval()
post = SoftPostProcessor()
class Generator:
def __init__(self, tok, mdl, pp):
self.tok = tok
self.mdl = mdl
self.pp = pp
def generate(self, title='', abstract=''):
text = f'keywords: {title} {abstract}'.strip()
inp = self.tok(text, max_length=512, truncation=True, return_tensors='pt')
with torch.no_grad():
out = self.mdl.generate(
**inp, max_new_tokens=128, do_sample=False,
no_repeat_ngram_size=4, repetition_penalty=1.5
)
pred = self.tok.decode(out[0], skip_special_tokens=True)
if pred.lower().startswith('keywords:'):
pred = pred[9:].strip()
kws = [k.strip() for k in pred.split(';') if k.strip()]
return self.pp.process(kws)
generator = Generator(byt5_tok, byt5_mdl, post)
log.info('ByT5 hazir')
clf = HibritSiniflandirici(
taxonomy, embedder,
keyword_weight=0.35, semantic_weight=0.65, title_boost=2.0
)
_init_ext(GEMINI_KEY, ALL_CATS)
log.info('Tum modeller hazir')
# ── FastAPI ──────────────────────────────────────────────────────────
@asynccontextmanager
async def lifespan(app: FastAPI):
load_models()
yield
app = FastAPI(title='LIFT UP Siniflandirici', lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=['*'],
allow_methods=['POST', 'GET'],
allow_headers=['*'],
)
class ClassifyRequest(BaseModel):
baslik: str
ozet: str
keywords: Optional[List[str]] = None
class KategoriResponse(BaseModel):
kategori: str
guven: float
keyword_skor: float
semantic_skor: float
eslesmeler: List[str]
class ClassifyResponse(BaseModel):
prediction: str
confidence: float
top_3: List[KategoriResponse]
extracted_keywords: List[str]
bert_keywords: List[str]
keybert_keywords: List[str]
byt5_keywords: List[str]
processing_time_ms: int
def bert_extract(text):
enc = bert_tok(
str(text).lower(), truncation=True,
padding='max_length', max_length=256, return_tensors='pt'
)
with torch.no_grad():
logits = bert_model(enc['input_ids'], enc['attention_mask'])
probs = torch.sigmoid(logits)[0].numpy()
idxs = np.argsort(probs)[-10:][::-1]
return [TOP_KEYWORDS[i] for i in idxs if probs[i] > 0.01][:5]
def keybert_extract(text):
clean = re.sub(r'[^\w\s\u011f\u011e\xfc\xdc\u015f\u015e\u0131\u0130\xf6\xd6\xe7\xc7]', ' ', text.lower()).strip()
try:
kws = kw_model.extract_keywords(
clean, keyphrase_ngram_range=(1, 3), top_n=5, use_mmr=True, diversity=0.2
)
return [k[0] for k in kws][:3]
except Exception:
return []
@app.get('/health')
def health():
return {'status': 'ok'}
@app.get('/')
def root():
return {'message': 'API calisiyor', 'endpoint': 'POST /classify'}
@app.post('/classify', response_model=ClassifyResponse)
def classify(req: ClassifyRequest):
if not req.baslik.strip() or not req.ozet.strip():
raise HTTPException(400, 'Baslik ve ozet zorunludur')
t0 = time.time()
text = f'{req.baslik} {req.ozet}'
bert_kws = bert_extract(text)
kb_kws = keybert_extract(text)
byt5_kws = generator.generate(req.baslik, req.ozet)
ext_kws = _ext_keywords(req.baslik, req.ozet)
extra = req.keywords or []
tum_kws = list(dict.fromkeys(bert_kws + kb_kws + byt5_kws + ext_kws + extra))
hybrid = clf.classify(tum_kws, text, title=req.baslik, top_k=3)
ext_cat = _ext_classify(req.baslik, req.ozet, ALL_CATS)
final = _combine_scores(hybrid, ext_cat)
ms = int((time.time() - t0) * 1000)
return ClassifyResponse(
prediction = final['prediction'],
confidence = round(final['confidence'], 4),
top_3 = [
KategoriResponse(
kategori = ks.kategori,
guven = round(ks.final_skor, 4),
keyword_skor = round(ks.keyword_skor, 4),
semantic_skor = round(ks.semantic_skor, 4),
eslesmeler = ks.eslesmeler[:5],
)
for ks in final['top_k']
],
extracted_keywords = tum_kws,
bert_keywords = bert_kws,
keybert_keywords = kb_kws,
byt5_keywords = byt5_kws,
processing_time_ms = ms,
)