JobSelect 0.10.2__py3-none-any.whl

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.
cli/jobselect.py ADDED
@@ -0,0 +1,114 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+ from pathlib import Path
5
+ import requests
6
+ from dotenv import load_dotenv
7
+ from rich import print
8
+ from pyfiglet import Figlet
9
+
10
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
11
+ if str(PROJECT_ROOT) not in sys.path:
12
+ sys.path.insert(0, str(PROJECT_ROOT))
13
+
14
+ _env_path = Path(__file__).resolve().parent / ".env"
15
+ if not _env_path.exists():
16
+ _env_path = PROJECT_ROOT / ".env"
17
+ load_dotenv(dotenv_path=_env_path)
18
+
19
+ API_URL = os.getenv("JOBAUTO_API_URL", "").rstrip("/")
20
+ API_KEY = os.getenv("JOBAUTO_API_KEY", "")
21
+
22
+ from model.pred import JobAnalyze_6k as _local_predict
23
+
24
+ def _call_api(jd: str, role: str, job_type: str) -> list[tuple[str, float]]:
25
+ endpoint = f"{API_URL}/JobAnalyze_6k"
26
+ headers = {"JobAnalyze_6k_Key": API_KEY}
27
+ payload = {"Job_Desc": jd, "Role": role, "Type": job_type}
28
+
29
+ response = requests.post(endpoint, json=payload, headers=headers, timeout=10)
30
+ response.raise_for_status()
31
+
32
+ data = response.json()
33
+
34
+ return [(skill, float(score)) for skill, score in data["answer"]]
35
+
36
+
37
+ def predict(jd: str, role: str, job_type: str) -> tuple[list[tuple[str, float]], str]:
38
+ if API_URL and API_KEY:
39
+ try:
40
+ results = _call_api(jd, role, job_type)
41
+ return results, "API"
42
+ except requests.exceptions.ConnectionError:
43
+ pass
44
+ except requests.exceptions.Timeout:
45
+ pass
46
+ except requests.exceptions.HTTPError as e:
47
+ if e.response is not None and e.response.status_code in (401, 403):
48
+ print(f"[red][JobAuto] API key issue ({e.response.status_code}) - running in LOCAL mode.[/red]")
49
+ except Exception:
50
+ pass
51
+
52
+ results = _local_predict(jd, role=role, job_type=job_type)
53
+ return results, "LOCAL"
54
+
55
+
56
+ def clear_console() -> None:
57
+ if os.name == "nt":
58
+ subprocess.run("cls", shell=True)
59
+ else:
60
+ subprocess.run(["clear"])
61
+
62
+
63
+ def title() -> None:
64
+ f = Figlet(font="slant")
65
+ print("——————————————————————————————————————————————————————————————————————")
66
+ print(f.renderText("JobSelect CLI"))
67
+ print("[blue] Running Model : JobAnalyze 6k v1.0")
68
+ print("Copyright Akshay Babu, All rights reserved")
69
+ print("——————————————————————————————————————————————————————————————————————")
70
+
71
+
72
+ def query(message: str) -> None:
73
+ print("\n[yellow][JobAuto]")
74
+ print(f"[yellow] >> {message} : ")
75
+ print("[You]")
76
+
77
+
78
+ def cli() -> None:
79
+ clear_console()
80
+ title()
81
+
82
+ print("[yellow][JobAuto] ")
83
+ print("[yellow]>> Welcome to JobAuto!")
84
+
85
+ query("Enter Job Description")
86
+ jd = input(" >> ")
87
+
88
+ query("Enter Role (AI Engineer / AI Developer)")
89
+ role = input(" >> ")
90
+
91
+ query("Enter Type (Internship / Junior / Senior)")
92
+ job_type = input(" >> ")
93
+
94
+ results, mode = predict(jd, role=role, job_type=job_type)
95
+
96
+ clear_console()
97
+ title()
98
+
99
+ mode_colour = "green" if mode == "API" else "cyan"
100
+ print(f"\n [{mode_colour}][Mode: {mode}][/{mode_colour}]")
101
+
102
+ print("\n [yellow]Job Description Provided : \n", jd.strip())
103
+ print("\n [yellow]Job Role : \n", role)
104
+ print("\n [yellow]Type : \n", job_type)
105
+ print()
106
+
107
+ print("\n [yellow]TOP Skills \n")
108
+ for label, prob in results:
109
+ bar = "█" * int(prob * 30)
110
+ print(f" {label:25s} {prob:.2f} {bar}\n")
111
+
112
+
113
+ if __name__ == "__main__":
114
+ cli()
@@ -0,0 +1,247 @@
1
+ Metadata-Version: 2.4
2
+ Name: JobSelect
3
+ Version: 0.10.2
4
+ Author-email: Akshay Babu <akshaysureshbabu100@gmail.com>
5
+ Description-Content-Type: text/markdown
6
+ License-File: LICENSE
7
+ Requires-Dist: rich==15.0.0
8
+ Requires-Dist: pyfiglet==1.0.4
9
+ Requires-Dist: scikit-learn==1.9.0
10
+ Requires-Dist: uvicorn==0.50.2
11
+ Requires-Dist: pydantic==2.13.4
12
+ Requires-Dist: requests==2.34.2
13
+ Requires-Dist: torch==2.12.1
14
+ Dynamic: license-file
15
+
16
+ # Job Description Skill Classifier [JobSeclect v0.10.2 & JobAnalyze 6k v1.0] (Multi-Label)
17
+
18
+ [![Python](https://img.shields.io/badge/Python-3.8+-3776AB?style=flat&logo=python)](https://www.python.org/)
19
+ [![PyTorch](https://img.shields.io/badge/PyTorch-2.12.1-%23EE4C2C?style=flat&logo=pytorch)](https://pytorch.org/)
20
+ [![scikit-learn](https://img.shields.io/badge/scikit--learn-1.9.0-F7931E?style=flat&logo=scikit-learn)](https://scikit-learn.org/)
21
+ [![NumPy](https://img.shields.io/badge/NumPy-2.4.6-013243?style=flat&logo=numpy)](https://numpy.org/)
22
+ [![Pandas](https://img.shields.io/badge/Pandas-3.0.3-150458?style=flat&logo=pandas)](https://pandas.pydata.org/)
23
+
24
+ This project builds a lightweight text classification pipeline that predicts **multiple technical skills** from a job posting. Given a job description (optionally augmented with role and job type), the model outputs a ranked list of likely skills.
25
+
26
+ Installation:
27
+
28
+ - `pip install jobselect`
29
+
30
+ It uses:
31
+
32
+ - **TF-IDF** features over the combined text (job description + role + type)
33
+ - A **PyTorch feed-forward neural network** trained as a **multi-label** classifier
34
+ - **Per-skill thresholding** for evaluation and **top-k ranked probabilities** for inference
35
+
36
+ ---
37
+
38
+ ## What it does
39
+
40
+ 1. **Data preparation** (`model/prep/data_prep.py`)
41
+ - Reads cleaned job description data.
42
+ - Normalizes/repairs common skill typos (e.g., `tesnorflow/pytorch` → `tensorflow/pytorch`).
43
+ - Builds a **multi-hot** target vector of skills.
44
+ - Fits a **TF-IDF** vectorizer (with n-grams) and splits into train/test.
45
+ - Saves:
46
+ - `model/prep/prepared_data.npz` (TF-IDF arrays + labels + indexes)
47
+ - `model/prep/vectorizer.pkl` (fitted TF-IDF vectorizer)
48
+ - `model/prep/label_vocab.json` (skill label vocabulary)
49
+
50
+ 2. **Model training** (`model/model.py`)
51
+ - Loads prepared TF-IDF arrays.
52
+ - Defines a simple **MLP**:
53
+ - Linear → ReLU → Dropout → Linear (one logit per skill)
54
+ - Trains with `BCEWithLogitsLoss` (multi-label setting).
55
+ - Saves:
56
+ - `model_out/skill_classifier.pt` (model weights)
57
+ - `model_out/training_history.json` (train/test loss curves)
58
+
59
+ 3. **Evaluation** (`model/eval.py`)
60
+ - Loads the trained model.
61
+ - Applies a fixed sigmoid + threshold (**0.5**) to obtain binary skill predictions.
62
+ - Reports:
63
+ - Per-skill precision/recall/F1
64
+ - Micro-F1 and Macro-F1
65
+ - Compares against a simple baseline (frequency-driven / always-predict-most-frequent labels).
66
+
67
+ 4. **Prediction / Inference** (`model/pred.py`)
68
+ - Loads the TF-IDF vectorizer and trained model.
69
+ - Creates TF-IDF features for the input text.
70
+ - Outputs the **top-k** skills by probability.
71
+
72
+ ---
73
+
74
+ ## Use cases
75
+
76
+ - **Resume/job-post matching** (first-pass filtering of relevant skills)
77
+ - **Job taxonomy building** (discover recurring skills from postings)
78
+ - **Recruiting analytics** (aggregate predicted skill demand by seniority/role/type)
79
+ - **Prototyping multi-label NLP classifiers** (TF-IDF + MLP baseline)
80
+
81
+ ---
82
+
83
+ ## Project structure
84
+
85
+ ```text
86
+ .
87
+ ├─ data/
88
+ │ ├─ raw/
89
+ │ │ └─ Job_descriptions.csv # Raw input dataset
90
+ │ ├─ sample_data/
91
+ │ │ └─ test.txt # Example JD, Role and Type for testing
92
+ │ └─ clean/
93
+ │ ├─ cleaned_job_descriptions.csv # Cleaned master CSV
94
+ │ ├─ cleaned_job_descriptions_internships.csv
95
+ │ ├─ cleaned_job_descriptions_junior.csv
96
+ │ └─ cleaned_job_descriptions_senior.csv
97
+
98
+ ├─ model/
99
+ │ ├─ prep/
100
+ │ │ ├─ data_prep.py # TF-IDF + multi-hot label creation + train/test split
101
+ │ │ ├─ sym_map.py # Synonym/phrase normalization map used during prep
102
+ │ │ ├─ vectorizer.pkl # Saved TF-IDF vectorizer (generated by data_prep)
103
+ │ │ ├─ prepared_data.npz # Saved arrays (generated by data_prep)
104
+ │ │ └─ label_vocab.json # Skill label vocabulary (generated by data_prep)
105
+ │ │
106
+ │ ├─ model.py # PyTorch multi-label classifier training
107
+ │ ├─ eval.py # Thresholded evaluation + F1 metrics + baseline comparison
108
+ │ └─ pred.py # Predict top-k skills for new text
109
+
110
+ ├─ model_out/
111
+ │ ├─ skill_classifier.pt # Trained model weights (generated by model.py)
112
+ │ └─ training_history.json # Training loss history (generated by model.py)
113
+
114
+ ├─ cli/
115
+ │ └─ jobselect.py # Rich terminal CLI for interactive prediction (uses model.pred.predict)
116
+
117
+ ├─ test/
118
+ │ └─ test_model.py # Pytest checks expected artifacts exist in model_out/ and model/prep/
119
+
120
+ ├─ notebooks/
121
+ │ ├─ 01_EDA.ipynb # Exploratory Data Analysis
122
+ │ └─ 02_Data_Engineering.ipynb # Data engineering / cleaning notes
123
+
124
+ ├─ pipeline.py # Executes notebooks + training/eval steps in order
125
+ ├─ pyproject.toml # Installs as a cli tool
126
+ ├─ requirements.txt
127
+ └─ README.md
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Requirements
133
+
134
+ See `requirements.txt` for the exact dependencies.
135
+
136
+ ---
137
+
138
+ ## Getting started
139
+
140
+ ### 1) Clone and Install dependencies
141
+
142
+ ```bash
143
+ git clone https://github.com/Ak47xdd/Job-Description-Analysis.git
144
+ pip install -r requirements.txt
145
+ ```
146
+
147
+ ### 2) Run data preparation (optional)
148
+
149
+ This builds the TF-IDF features and label vocabulary from the cleaned CSV.
150
+
151
+ ```bash
152
+ python model/prep/data_prep.py
153
+ ```
154
+
155
+ Expected outputs:
156
+
157
+ - `model/prep/prepared_data.npz`
158
+ - `model/prep/vectorizer.pkl`
159
+ - `model/prep/label_vocab.json`
160
+
161
+ ### 3) Train the model (optional)
162
+
163
+ ```bash
164
+ python model/model.py
165
+ ```
166
+
167
+ Expected outputs:
168
+
169
+ - `model_out/skill_classifier.pt`
170
+ - `model_out/training_history.json`
171
+
172
+ ### 4) Evaluate performance (optional)
173
+
174
+ ```bash
175
+ python model/eval.py
176
+ ```
177
+
178
+ Outputs include:
179
+
180
+ - Per-skill metrics (precision/recall/F1)
181
+ - Micro-F1 and Macro-F1
182
+ - Baseline comparison
183
+
184
+ ### 5) Predict skills for a new job description
185
+
186
+ #### Option A: Use Python function
187
+
188
+ `from model.pred import predict`
189
+
190
+ `data/sample_data/test.txt` contains an example job description inside. You can also call the `predict(job_desc, role=..., job_type=..., top_k=...)` function from Python.
191
+
192
+ #### Option B: Use the interactive CLI
193
+
194
+ ```bash
195
+ python cli/jobselect.py
196
+
197
+ or
198
+
199
+ pip install jobselect
200
+ jobselect
201
+ ```
202
+
203
+ The CLI prompts for job description, role, and job type, then prints the top skills with probabilities.
204
+
205
+ ---
206
+
207
+ ## How predictions work
208
+
209
+ - Text is concatenated as:
210
+ `"{job_desc} {role} {job_type}"`
211
+ - TF-IDF transforms text into a fixed-size vector
212
+ - The network outputs one logit per skill
213
+ - Sigmoid converts logits → probabilities
214
+ - Skills are ranked by probability and the top-k are returned
215
+
216
+ ---
217
+
218
+ ## Important implementation notes
219
+
220
+ - **Multi-label learning:** Each skill is treated independently (binary relevance via sigmoid + `BCEWithLogitsLoss`).
221
+ - **Evaluation threshold:** `model/eval.py` uses a fixed threshold of **0.5**. For production use, you may want per-label thresholds tuned on a validation set.
222
+ - **Dataset size:** The included notebooks and evaluation code suggest the dataset may be small; results can be limited by label frequency and data coverage.
223
+
224
+ ---
225
+
226
+ ## New features / capabilities
227
+
228
+ - **Rich terminal CLI** (`cli/jobauto.py`) using `rich` + `pyfiglet` for interactive top-skill display.
229
+ - **Synonym/phrase normalization hook** (`model/prep/sym_map.py`) applied during data preparation.
230
+ - **Pipeline runner** (`pipeline.py`) to execute notebooks and training steps in sequence.
231
+
232
+ ---
233
+
234
+ ## Customization ideas
235
+
236
+ - Improve text cleaning and skill normalization in `data_prep.py`
237
+ - Tune TF-IDF parameters (`max_features`, `ngram_range`, `min_df`)
238
+ - Replace the simple MLP with a stronger baseline (e.g., logistic regression on TF-IDF)
239
+ - Calibrate thresholds per label using validation data
240
+ - Add a CLI or web service endpoint for prediction
241
+
242
+ ---
243
+
244
+ ## References / Inspiration
245
+
246
+ This repository follows a common pattern for multi-label NLP baselines:
247
+ TF-IDF features + a simple neural network + sigmoid-based multi-label outputs.
@@ -0,0 +1,20 @@
1
+ cli/jobselect.py,sha256=zTCUz4h_z2TC43rFXeZhzQDlKnxqTj_3lxKj4bDVcYE,3767
2
+ jobselect-0.10.2.dist-info/licenses/LICENSE,sha256=GJCmsVy7uDWkdHQkyHmRY0-poSNCBYKMVkxsWu6qs7M,1089
3
+ model/Model.py,sha256=Q8sZ-zWd_DPiEtz4inD2pixU1FGu5dFDTIGEEfuNkdY,4017
4
+ model/__init__.py,sha256=FqtAGX76Pn5t68GeapJMUuKKg5lAan4ln8qdYHP7Joc,251
5
+ model/eval.py,sha256=K_79cWtKihtCoisiei60SitzxUvh-FClWIfYyvZzFDQ,3180
6
+ model/pred.py,sha256=xGIbg_5OPOEYXIGgTjJRYrJDg6MKp3WSdRBis2L5EUo,2403
7
+ model/prep/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ model/prep/data_prep.py,sha256=8pjiAeqPuUWWeHqsxY8jO5nFo7kuc4hrk1IeTM3fW-I,2748
9
+ model/prep/label_vocab.json,sha256=8BPIqMeYhM0qXQf4vt0DI3Z0cwfmSGEveJ9qOle_AwM,694
10
+ model/prep/prepared_data.npz,sha256=QFEpBmYszlijDf5wL-YhHtJjLsUHTtCaX0a-7U3qOfw,169500
11
+ model/prep/sym_map.py,sha256=1ToC-zE8s2jx_Zw8MxPYTzutam9pvcAgHfQpNmb1BF4,3645
12
+ model/prep/test.py,sha256=Fc4kaEPXFTkZNRwKCVZL9Azph0E1OIQVql3KqUTFERc,185
13
+ model/prep/vectorizer.pkl,sha256=ckdOX6V76Z3Y8RH2Uh_3M6r8nEebRGWfnSrnydf2tNQ,6583
14
+ model_out/skill_classifier.pt,sha256=Z4OvWe3pgJBSE5fkUTGsE29l4DXXxCUCfPkwfl5M160,28279
15
+ model_out/training_history.json,sha256=yCG5Kh36qUqEBDEU4dp2iK05VDQt57PqwGq4_q5D-Ws,11982
16
+ jobselect-0.10.2.dist-info/METADATA,sha256=LrY8WvtGvj3a4dAgA7BNC0XIDRmKUZEXNtzwekl4mk8,8958
17
+ jobselect-0.10.2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
18
+ jobselect-0.10.2.dist-info/entry_points.txt,sha256=VFcqEBmL3e4hu6N0fi545Qc47m388RVywebfXXzVQOg,48
19
+ jobselect-0.10.2.dist-info/top_level.txt,sha256=SckdL5L6cqX90MVRKonPOb125RLJ-gkPgFgD2_5JQjk,20
20
+ jobselect-0.10.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ jobselect = cli.jobselect:cli
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Akshay_Babu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ cli
2
+ model
3
+ model_out
model/Model.py ADDED
@@ -0,0 +1,123 @@
1
+ """
2
+ model.py - DO NOT RUN THIS SCRIPT!
3
+
4
+ Run this script, prep/data_prep.py and the notebooks through pipeline.py
5
+ """
6
+
7
+ import json
8
+ import torch
9
+ import numpy as np
10
+ import torch.nn as nn
11
+ from torch.utils.data import Dataset, DataLoader
12
+ from pathlib import Path
13
+
14
+ REPO_ROOT = Path(__file__).resolve().parent.parent
15
+
16
+ torch.manual_seed(42)
17
+
18
+ DATA_DIR = Path(__file__).resolve().parent / 'prep'
19
+
20
+ # Ensure required artifacts exist even when run from different CWDs
21
+ assert (DATA_DIR / 'prepared_data.npz').exists(), f"Missing {DATA_DIR / 'prepared_data.npz'}"
22
+ assert (DATA_DIR / 'label_vocab.json').exists(), f"Missing {DATA_DIR / 'label_vocab.json'}"
23
+
24
+ data = np.load(DATA_DIR / 'prepared_data.npz')
25
+
26
+ X_train = data['X_train']
27
+ X_test = data['X_test']
28
+ y_train = data['y_train']
29
+ y_test = data['y_test']
30
+
31
+ with open(DATA_DIR / 'label_vocab.json', encoding='utf-8') as f:
32
+ VOCAB = json.load(f)
33
+
34
+
35
+ DIM = X_train.shape[1]
36
+ NUM_LABELS = len(VOCAB)
37
+
38
+ class SkillData(Dataset):
39
+ def __init__(self, X, y) -> torch.tensor:
40
+ self.X = torch.tensor(X, dtype=torch.float32)
41
+ self.y = torch.tensor(y, dtype=torch.float32)
42
+
43
+ def __len__(self) -> len:
44
+ return len(self.X)
45
+
46
+ def __getitem__(self, idx) -> int:
47
+ return self.X[idx], self.y[idx]
48
+
49
+ train_ds = SkillData(X_train, y_train)
50
+ test_ds = SkillData(X_test, y_test)
51
+
52
+ train_loader = DataLoader(train_ds, batch_size=64, shuffle=True)
53
+ test_loader = DataLoader(test_ds, batch_size=64, shuffle=False)
54
+
55
+ class SkillClassifier(nn.Module):
56
+ def __init__(self, input_dim, num_labels, hidden_dim=32, dropout=0.3) -> None:
57
+ super().__init__()
58
+ self.net = nn.Sequential(
59
+ nn.Linear(input_dim, hidden_dim),
60
+ nn.ReLU(),
61
+ nn.Dropout(dropout),
62
+ nn.Linear(hidden_dim, num_labels)
63
+ )
64
+
65
+ def forward(self, x) -> float:
66
+ return self.net(x)
67
+
68
+ jobanalyze_6k = SkillClassifier(DIM, NUM_LABELS)
69
+
70
+ total_params = sum(p.numel() for p in jobanalyze_6k.parameters())
71
+ trainable_params = sum(p.numel() for p in jobanalyze_6k.parameters() if p.requires_grad)
72
+
73
+ print(f"Total Parameters: {total_params:,}")
74
+ print(f"Trainable Parameters: {trainable_params:,}")
75
+
76
+ pos_counts = y_train.sum(axis=0)
77
+ neg_counts = len(y_train) - pos_counts
78
+ pos_weight = torch.tensor(neg_counts / (pos_counts + 1e-6), dtype=torch.float32)
79
+ pos_weight = torch.clamp(pos_weight, max=10.0)
80
+
81
+ criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)
82
+ optimizer = torch.optim.Adam(jobanalyze_6k.parameters(), lr=1e-3, weight_decay=1e-4)
83
+
84
+ EPOCHS = 300
85
+ history = {'train_loss' : [], 'test_loss' : []}
86
+
87
+ for epoch in range(1, EPOCHS + 1):
88
+ jobanalyze_6k.train()
89
+ train_losses = []
90
+ for xb, yb in train_loader:
91
+ optimizer.zero_grad()
92
+ logits = jobanalyze_6k(xb)
93
+ loss = criterion(logits, yb)
94
+ loss.backward()
95
+ optimizer.step()
96
+ train_losses.append(loss.item())
97
+
98
+ jobanalyze_6k.eval()
99
+ test_losses = []
100
+ with torch.no_grad():
101
+ for xb, yb in test_loader:
102
+ logits = jobanalyze_6k(xb)
103
+ loss = criterion(logits, yb)
104
+ test_losses.append(loss.item())
105
+
106
+ train_loss = sum(train_losses) / len(train_losses)
107
+ test_loss = sum(test_losses) / len(test_losses)
108
+ history['train_loss'].append(train_loss)
109
+ history['test_loss'].append(test_loss)
110
+
111
+ if epoch % 10 == 0 or epoch == 1:
112
+ print(f"Epoch {epoch:3d} | train_loss {train_loss:.4f} | test_loss {test_loss:.4f}")
113
+
114
+ out_dir = REPO_ROOT / 'model_out'
115
+ out_dir.mkdir(parents=True, exist_ok=True)
116
+
117
+ torch.save(jobanalyze_6k.state_dict(), out_dir / 'skill_classifier.pt')
118
+ with open(out_dir / 'training_history.json', 'w') as f:
119
+ json.dump(history, f)
120
+
121
+ print("\n Saved Model")
122
+ print(f"Final train_loss : {history['train_loss'][-1]:.4f} | "
123
+ f"Final test_loss : {history['test_loss'][-1]:.4f}")
model/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Model package.
2
+
3
+ Exporting SkillClassifier directly from `model.py` triggers training-time
4
+ side effects (loading datasets) at import time, which breaks CLI usage.
5
+
6
+ CLI code should import from `model.pred` / `model.model` as needed.
7
+ """
8
+
9
+
10
+
model/eval.py ADDED
@@ -0,0 +1,106 @@
1
+ """
2
+ eval.py - RUN after pipeline.py
3
+
4
+ evaluation metrics for the model
5
+ """
6
+
7
+ import json
8
+ import numpy as np
9
+ import torch
10
+ from sklearn.metrics import precision_recall_fscore_support, f1_score, accuracy_score
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ ROOT = Path(__file__).resolve().parent.parent
15
+
16
+ from Model import SkillClassifier
17
+
18
+ sys.path.insert(0, '.')
19
+
20
+ data = np.load('prep/prepared_data.npz')
21
+
22
+ X_train = data['X_train']
23
+ y_train = data['y_train']
24
+ X_test = data['X_test']
25
+ y_test = data['y_test']
26
+
27
+ with open('prep/label_vocab.json') as f:
28
+ VOCAB = json.load(f)
29
+
30
+ model = SkillClassifier(X_train.shape[1], len(VOCAB))
31
+ model.load_state_dict(torch.load(ROOT / 'model_out' / 'skill_classifier.pt', map_location='cpu'))
32
+
33
+ model.eval()
34
+
35
+ with torch.no_grad():
36
+ logits = model(torch.tensor(X_test, dtype=torch.float32))
37
+ probs = torch.sigmoid(logits).numpy()
38
+
39
+ THRESHOLD = 0.5
40
+ preds = (probs >= THRESHOLD).astype(int)
41
+
42
+ precision, recall, f1, support = precision_recall_fscore_support(
43
+ y_test,
44
+ preds,
45
+ average=None,
46
+ zero_division=0
47
+ )
48
+ micro_f1 = f1_score(y_test, preds, average='micro', zero_division=0)
49
+ macro_f1 = f1_score(y_test, preds, average='macro', zero_division=0)
50
+
51
+ print("\n Model Evaluation Metrics\n")
52
+ print(f"{'label':25s}{'support':10s}{'precision':12s}{'recall':10s}{'f1':6s}")
53
+ for lbl, p, r, f, s in zip(VOCAB, precision, recall, f1, support):
54
+ if s > 0:
55
+ print(f"{lbl:25s}{int(s):<10d}{p:<12.2f}{r:<10.2f}{f:.2f}")
56
+
57
+ print(f"\nMicro-F1: {micro_f1:.3f} | Macro-F1: {macro_f1:.3f}")
58
+
59
+ print("\n=== PER-LABEL ACCURACY ===")
60
+ print(f"{'label':25s}{'accuracy%':12s}{'support':10s}{'trap?':6s}")
61
+
62
+ is_right = 0
63
+ is_wrong = 0
64
+
65
+ for i, lbl in enumerate(VOCAB):
66
+ label_acc = accuracy_score(y_test[:, i], preds[:, i])
67
+ s = int(support[i])
68
+
69
+ always_zero_acc = 1.0 - (y_test[:, i].sum() / len(y_test))
70
+ is_trap = always_zero_acc >= label_acc - 0.01
71
+
72
+ trap_flag = "Wrong" if is_trap else "Right"
73
+ print(f"{lbl:25s}{label_acc*100:<12.1f}{s:<10d}{trap_flag}")
74
+
75
+ if trap_flag == 'Right':
76
+ is_right += 1
77
+ else:
78
+ is_wrong += 1
79
+
80
+ total_labels = is_right + is_wrong
81
+
82
+ print("Right : \n", is_right)
83
+ print("Wrong : \n", is_wrong)
84
+
85
+ print("Total Labels : \n", total_labels)
86
+
87
+ key_acc = (is_right / total_labels) * 100
88
+
89
+ print(f"Keyword Accuracy : {round(key_acc, 2)}%\n")
90
+
91
+ train_freq = y_train.mean(axis=0)
92
+ baseline_preds = np.tile((train_freq >= 0.3).astype(int), (len(y_test), 1))
93
+
94
+ baseline_micro_f1 = f1_score(y_test, baseline_preds, average='micro', zero_division=0)
95
+ baseline_macro_f1 = f1_score(y_test, baseline_preds, average='macro', zero_division=0)
96
+
97
+ print(f"\nBASELINE: \n")
98
+ baseline_labels = [lbl for lbl, f in zip(VOCAB, train_freq) if f >= 0.3]
99
+ print(f"Baseline always predicts: {baseline_labels}")
100
+ print(f"Baseline Micro-F1: {baseline_micro_f1:.3f} | Macro-F1: {baseline_macro_f1:.3f}")
101
+
102
+ print("\nVERDICT \n")
103
+ if micro_f1 > baseline_micro_f1 + 0.05:
104
+ print("Model meaningfully beats the naive baseline.")
105
+ else:
106
+ print("Model is roughly tied with (or worse than) just guessing the most")
model/pred.py ADDED
@@ -0,0 +1,81 @@
1
+ """
2
+ Pred Module
3
+ """
4
+
5
+
6
+ import json
7
+ import pickle
8
+ from typing import List, Tuple
9
+ import numpy as np
10
+ import torch
11
+ from pathlib import Path
12
+
13
+ ROOT = Path(__file__).resolve().parent.parent
14
+
15
+ from torch import nn
16
+
17
+ class SkillClassifier(nn.Module):
18
+ def __init__(self, input_dim, num_labels, hidden_dim=32, dropout=0.3):
19
+ super().__init__()
20
+ self.net = nn.Sequential(
21
+ nn.Linear(input_dim, hidden_dim),
22
+ nn.ReLU(),
23
+ nn.Dropout(dropout),
24
+ nn.Linear(hidden_dim, num_labels),
25
+ )
26
+
27
+ def forward(self, x):
28
+ return self.net(x)
29
+
30
+ def JobAnalyze_6k(job_desc: str = "", role: str = "", job_type: str = "", top_k: int = 50) -> List[Tuple[str, float]]:
31
+ """
32
+ Predict top-k skills.
33
+
34
+ Current top-k = 48
35
+ """
36
+ prep_dir = ROOT / "model" / "prep"
37
+ if not prep_dir.exists():
38
+ alt = ROOT / "prep"
39
+ if alt.exists():
40
+ prep_dir = alt
41
+
42
+ label_path = prep_dir / "label_vocab.json"
43
+ vector_path = prep_dir / "vectorizer.pkl"
44
+ weights_path = ROOT / "model_out" / "skill_classifier.pt"
45
+
46
+ if not label_path.exists():
47
+ raise FileNotFoundError(
48
+ f"Missing {label_path}. Make sure you ran data_prep and model training."
49
+ )
50
+ if not vector_path.exists():
51
+ raise FileNotFoundError(
52
+ f"Missing {vector_path}. Make sure you ran data_prep."
53
+ )
54
+ if not weights_path.exists():
55
+ raise FileNotFoundError(
56
+ f"Missing {weights_path}. Make sure you ran model/model.py."
57
+ )
58
+
59
+ with open(label_path, encoding="utf-8") as f:
60
+ label_vocab = json.load(f)
61
+ with open(vector_path, "rb") as f:
62
+ vectorizer = pickle.load(f)
63
+
64
+ input_dim = int(getattr(vectorizer, "vocabulary_", {}).__len__()) or vectorizer.transform([""]).shape[1]
65
+
66
+ model = SkillClassifier(input_dim, len(label_vocab))
67
+ model.load_state_dict(torch.load(weights_path, map_location="cpu"))
68
+
69
+ model.eval()
70
+
71
+ combined_text = f"{job_desc} {role} {job_type}"
72
+ X = vectorizer.transform([combined_text]).toarray().astype(np.float32)
73
+
74
+ with torch.no_grad():
75
+ logits = model(torch.tensor(X))
76
+ probs = torch.sigmoid(logits).numpy()[0]
77
+
78
+ ranked = sorted(zip(label_vocab, probs), key=lambda x: -x[1])
79
+ return ranked[:top_k]
80
+
81
+
model/prep/__init__.py ADDED
File without changes
@@ -0,0 +1,95 @@
1
+ """
2
+ data_prep.py - DO NOT RUN THIS SCRIPT!
3
+
4
+ Run this script, model/model.py and the notebooks through pipeline.py
5
+ """
6
+
7
+ import json
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+ import pickle
13
+ from collections import Counter
14
+ from sklearn.feature_extraction.text import TfidfVectorizer
15
+ from sklearn.model_selection import train_test_split
16
+
17
+ from sym_map import SYNONYM_MAP
18
+
19
+
20
+ df = pd.read_csv(r'C:\Portfolio-Projects\Job-Description-Analysis\data\clean\cleaned_job_descriptions.csv')
21
+
22
+ SKILLS_FIX = {
23
+ 'tesnorflow/pytorch': 'tensorflow/pytorch',
24
+ 'numpyhugging face': 'numpy',
25
+ 'sytem design': 'system design',
26
+ 'python. ml': 'ml',
27
+ }
28
+
29
+ def normalizer(skills) -> list:
30
+ if pd.isna(skills):
31
+ return []
32
+ skill = [s.strip().lower() for s in skills.split(',') if s.strip()]
33
+ fixed = [SKILLS_FIX.get(s, s) for s in skill]
34
+ return list(dict.fromkeys(fixed))
35
+
36
+ df['skill_list'] = df['tech_skills'].apply(normalizer)
37
+
38
+ freq = Counter(s for lst in df['skill_list'] for s in lst)
39
+ VOCAB = sorted([s for s, c in freq.items() if c >= 2])
40
+
41
+ def encoder(skill_list) -> list:
42
+ return [1 if lbl in skill_list else 0 for lbl in VOCAB]
43
+
44
+ y = np.array([encoder(lst) for lst in df['skill_list']], dtype=np.float32)
45
+
46
+ def apply_synonyms(text: str) -> str:
47
+ text = text.lower()
48
+ for phrase, canonical in sorted(SYNONYM_MAP.items(), key=lambda x: -len(x[0])):
49
+ text = text.replace(phrase, canonical)
50
+ return text
51
+
52
+ jd_input = (
53
+ df['job_desc'].fillna('').apply(apply_synonyms) + ' ' +
54
+ df['role'].fillna('').str.lower() + ' ' +
55
+ df['type'].fillna('').str.lower()
56
+ )
57
+
58
+ vectorizer = TfidfVectorizer(
59
+ max_features=150,
60
+ stop_words='english',
61
+ ngram_range=(1, 2),
62
+ min_df=2,
63
+ )
64
+ X = vectorizer.fit_transform(jd_input).toarray().astype(np.float32)
65
+
66
+ X_train, X_test, y_train, y_test, idx_train, idx_test = train_test_split(
67
+ X, y, np.arange(len(df)),
68
+ test_size=0.2,
69
+ random_state=42
70
+ )
71
+
72
+ print(f"Dataset: {len(df)} rows | Vocab: {len(VOCAB)} labels | "
73
+ f"TF-IDF features: {X.shape[1]}")
74
+ print(f"Train: {len(X_train)} | Test: {len(X_test)}")
75
+
76
+ # Save artifacts relative to this script location so pipeline.py can be run from repo root
77
+ OUT_DIR = Path(__file__).resolve().parent
78
+
79
+ np.savez(
80
+ OUT_DIR / 'prepared_data.npz',
81
+ X_train=X_train,
82
+ X_test=X_test,
83
+ y_train=y_train,
84
+ y_test=y_test,
85
+ idx_train=idx_train,
86
+ idx_test=idx_test,
87
+ )
88
+
89
+ with open(OUT_DIR / 'label_vocab.json', 'w') as f:
90
+ json.dump(VOCAB, f, indent=2)
91
+
92
+ with open(OUT_DIR / 'vectorizer.pkl', 'wb') as f:
93
+ pickle.dump(vectorizer, f)
94
+
95
+ print("Successfully Vectorized and Pickled Data")
@@ -0,0 +1,50 @@
1
+ [
2
+ ".net",
3
+ "agents",
4
+ "anthropic /openai sdks",
5
+ "apis",
6
+ "autogen",
7
+ "aws/azure",
8
+ "c",
9
+ "c#",
10
+ "c++",
11
+ "ci/cd",
12
+ "crewai",
13
+ "django",
14
+ "docker",
15
+ "feature engineering",
16
+ "full stack",
17
+ "genai",
18
+ "git",
19
+ "github",
20
+ "hugging face",
21
+ "java",
22
+ "javascript",
23
+ "kubernetes",
24
+ "langchain",
25
+ "langgraph",
26
+ "llamaindex",
27
+ "llms",
28
+ "mcp",
29
+ "ml",
30
+ "mlflow",
31
+ "mlops",
32
+ "model evaluation",
33
+ "model training",
34
+ "n8n",
35
+ "nlp",
36
+ "numpy",
37
+ "openai",
38
+ "pandas",
39
+ "powerbi",
40
+ "prompt engineering",
41
+ "python",
42
+ "r",
43
+ "rag",
44
+ "react",
45
+ "scikit-learn",
46
+ "sql",
47
+ "system design",
48
+ "tensorflow/pytorch",
49
+ "vectordb"
50
+ ]
Binary file
model/prep/sym_map.py ADDED
@@ -0,0 +1,106 @@
1
+ SYNONYM_MAP = {
2
+ # docker
3
+ 'containeriz': 'docker',
4
+ 'containerize': 'docker',
5
+
6
+ # git
7
+ 'version control': 'git',
8
+ 'source control': 'git',
9
+
10
+ # github
11
+ 'gitlab': 'github',
12
+ 'bitbucket': 'github',
13
+ 'pull request': 'github',
14
+
15
+ # ci/cd
16
+ 'infrastructure-as-code': 'ci/cd',
17
+ 'continuous integration': 'ci/cd',
18
+ 'continuous deployment': 'ci/cd',
19
+ 'continuous delivery': 'ci/cd',
20
+ 'github actions': 'ci/cd',
21
+ 'gitlab ci': 'ci/cd',
22
+ 'jenkins': 'ci/cd',
23
+ 'devops': 'ci/cd',
24
+ 'cicd': 'ci/cd',
25
+
26
+ # genai
27
+ 'generative ai': 'genai',
28
+ 'frontier model': 'genai',
29
+ 'foundation model': 'genai',
30
+ 'gen ai': 'genai',
31
+ 'gemini': 'genai',
32
+ 'claude': 'genai',
33
+ 'gpt': 'genai',
34
+
35
+ # agents
36
+ 'multi-agent': 'agents',
37
+ 'orchestrat': 'agents',
38
+ 'agentic': 'agents',
39
+ 'copilot': 'agents',
40
+
41
+ # mcp
42
+ 'model context protocol': 'mcp',
43
+ 'protocol wrapper': 'mcp',
44
+ 'function calling': 'mcp',
45
+ 'tool call': 'mcp',
46
+
47
+ # prompt engineering
48
+ 'prompt engineer': 'prompt engineering',
49
+ 'chain-of-thought': 'prompt engineering',
50
+ 'system prompt': 'prompt engineering',
51
+ 'few-shot': 'prompt engineering',
52
+ 'zero-shot': 'prompt engineering',
53
+ 'prompt design': 'prompt engineering',
54
+
55
+ # mlops
56
+ 'experiment track': 'mlops',
57
+ 'model monitor': 'mlops',
58
+ 'model deploy': 'mlops',
59
+ 'model registry': 'mlops',
60
+ 'ml ops': 'mlops',
61
+
62
+ # kubernetes
63
+ 'k8s': 'kubernetes',
64
+ 'helm': 'kubernetes',
65
+ 'eks': 'kubernetes',
66
+ 'aks': 'kubernetes',
67
+ 'gke': 'kubernetes',
68
+
69
+ # sql
70
+ 'postgresql': 'sql',
71
+ 'postgres': 'sql',
72
+ 'pgvector': 'sql',
73
+ 'snowflake': 'sql',
74
+ 'databricks': 'sql',
75
+ 'mysql': 'sql',
76
+ 'nosql': 'sql',
77
+
78
+ # vectordb
79
+ 'azure ai search': 'vectordb',
80
+ 'vector database': 'vectordb',
81
+ 'vector store': 'vectordb',
82
+ 'embedding store': 'vectordb',
83
+ 'pinecone': 'vectordb',
84
+ 'chromadb': 'vectordb',
85
+ 'weaviate': 'vectordb',
86
+ 'qdrant': 'vectordb',
87
+ 'faiss': 'vectordb',
88
+ 'milvus': 'vectordb',
89
+
90
+ # nlp
91
+ 'natural language': 'nlp',
92
+ 'named entity': 'nlp',
93
+ 'text classification': 'nlp',
94
+
95
+ # aws/azure
96
+ 'amazon web services': 'aws/azure',
97
+ 'azure openai': 'aws/azure',
98
+ 'google cloud': 'aws/azure',
99
+ 'ai foundry': 'aws/azure',
100
+ 'sagemaker': 'aws/azure',
101
+ 'bedrock': 'aws/azure',
102
+ 'gcp': 'aws/azure',
103
+ 'lambda': 'aws/azure',
104
+ 'ec2': 'aws/azure',
105
+ 's3': 'aws/azure',
106
+ }
model/prep/test.py ADDED
@@ -0,0 +1,5 @@
1
+ import pickle
2
+ with open('vectorizer.pkl', 'rb') as f:
3
+ v = pickle.load(f)
4
+ print('ci/cd' in v.get_feature_names_out())
5
+ print('infrastructure-as-code' in v.get_feature_names_out())
Binary file
Binary file
@@ -0,0 +1 @@
1
+ {"train_loss": [0.9581862886746725, 0.9577147165934244, 0.9461615284283956, 0.9476661086082458, 0.9445720911026001, 0.9469820857048035, 0.9392802516619364, 0.9505228598912557, 0.950762927532196, 0.936366856098175, 0.9329907099405924, 0.9334798057874044, 0.9212138652801514, 0.916998823483785, 0.9186211029688517, 0.9199942350387573, 0.917285700639089, 0.9141116539637247, 0.9081712365150452, 0.9007992744445801, 0.8928503592809042, 0.8881315986315409, 0.8868852655092875, 0.8855883677800497, 0.8726210395495096, 0.8689012924830118, 0.8664668202400208, 0.8664715886116028, 0.858808716138204, 0.8502107262611389, 0.8514769673347473, 0.8443883856137594, 0.8354653914769491, 0.8419389526049296, 0.8347751895586649, 0.8293438951174418, 0.8314108848571777, 0.8242264787356058, 0.8150125741958618, 0.8148065209388733, 0.8208412528038025, 0.8038379549980164, 0.8083155353864034, 0.7979742487271627, 0.8024805585543314, 0.8121252655982971, 0.8099909623463949, 0.7862127621968588, 0.7826682726542155, 0.7992724378903707, 0.7828892866770426, 0.7740360895792643, 0.7762934565544128, 0.7719021240870158, 0.7708912889162699, 0.7710039615631104, 0.7656899094581604, 0.7630510330200195, 0.7667286992073059, 0.7553671598434448, 0.7476920684178671, 0.7605920235315958, 0.7457097371419271, 0.7439080874125162, 0.7375965317090353, 0.7373116413752238, 0.740321417649587, 0.7358438372612, 0.7284310857454935, 0.7350131670633951, 0.7183190186818441, 0.7204438249270121, 0.7150562802950541, 0.7068103353182474, 0.7275595664978027, 0.7266760071118673, 0.7168972492218018, 0.7118367950121561, 0.7136472860972086, 0.7103650569915771, 0.6976963678995768, 0.7045106093088785, 0.6940693457921346, 0.687540332476298, 0.698395570119222, 0.6792084574699402, 0.6907051205635071, 0.6839434107144674, 0.6806370615959167, 0.684864858786265, 0.6747912764549255, 0.6723761359850565, 0.6862576802571615, 0.6673139532407125, 0.6679207682609558, 0.6639275352160136, 0.6590360601743063, 0.6575275858243307, 0.6648958325386047, 0.6717456380526224, 0.6632175445556641, 0.6605779131253561, 0.6675432920455933, 0.6592809955279032, 0.6564900676409403, 0.6533124645551046, 0.6523601611455282, 0.6453654567400614, 0.6494061549504598, 0.6490419705708822, 0.6392372449239095, 0.6579065521558126, 0.6391883293787638, 0.6371342341105143, 0.6379399100939432, 0.6424667636553446, 0.6355656782786051, 0.6250395576159159, 0.6172210375467936, 0.6375317970911661, 0.6317365964253744, 0.6250279347101847, 0.6232229272524515, 0.6375943819681803, 0.6228700478871664, 0.6193240483601888, 0.6193757057189941, 0.6122732361157736, 0.6391211549441019, 0.6141518751780192, 0.6103077928225199, 0.6171488563219706, 0.6174906293551127, 0.6065569917360941, 0.6059826016426086, 0.6166791915893555, 0.5973984400431315, 0.6088824073473612, 0.6073964834213257, 0.6005466183026632, 0.6013992230097452, 0.6125759681065878, 0.6079150239626566, 0.5891634225845337, 0.6008321046829224, 0.5972617467244467, 0.5964677532513937, 0.5844391783078512, 0.5972137252489725, 0.5894015232721964, 0.6028069257736206, 0.592693050702413, 0.5975406964619955, 0.5897817015647888, 0.58717147509257, 0.5766614874204, 0.5890741149584452, 0.5749748945236206, 0.5733896891276041, 0.5756001273790995, 0.5863603154818217, 0.5884929100672404, 0.578599731127421, 0.5891632636388143, 0.5758164326349894, 0.5684518814086914, 0.5712788701057434, 0.5735013484954834, 0.5750335256258646, 0.5649053851763407, 0.5641095836957296, 0.5632531642913818, 0.5593478083610535, 0.5687779784202576, 0.565412183602651, 0.566339115301768, 0.5505285859107971, 0.5565174619356791, 0.556056539217631, 0.5571393171946207, 0.5519521633783976, 0.5626179178555807, 0.5540618896484375, 0.5583128929138184, 0.5578981041908264, 0.5443036357561747, 0.5576624075571696, 0.5645721753438314, 0.5607338547706604, 0.5481768647829691, 0.534951368967692, 0.5381232698758444, 0.5366830428441366, 0.5464838743209839, 0.5413755575815836, 0.5512260794639587, 0.543988823890686, 0.5405274629592896, 0.5418429176012675, 0.5398401021957397, 0.5361861785252889, 0.5481286843617758, 0.5376873215039571, 0.5290207862854004, 0.5361277262369791, 0.5333669185638428, 0.5322141846021017, 0.5257742206255595, 0.5330172181129456, 0.5436073342959086, 0.5269099473953247, 0.5185351669788361, 0.5202850103378296, 0.5281335214773814, 0.5258443653583527, 0.5325279434521993, 0.5321405132611593, 0.5215916931629181, 0.5315477848052979, 0.5168627699216207, 0.5142837762832642, 0.51143017411232, 0.5149370034535726, 0.5177168250083923, 0.5222222010294596, 0.5185661117235819, 0.5199925303459167, 0.5164631207784017, 0.522129108508428, 0.5196901758511862, 0.5083784560362498, 0.5147261520226797, 0.5110023319721222, 0.5160118043422699, 0.5126267770926157, 0.5193240543206533, 0.5121001203854879, 0.5038588245709738, 0.4998914698759715, 0.5149586896101633, 0.5078332523504893, 0.513956199089686, 0.49984930952390033, 0.4997527798016866, 0.5031786362330118, 0.4964219530423482, 0.4983686606089274, 0.49576592445373535, 0.49676430225372314, 0.5016859471797943, 0.5083380540211996, 0.49582229057947796, 0.5047439734141032, 0.5027091801166534, 0.49981353680292767, 0.49778684973716736, 0.4949717919031779, 0.49530373016993207, 0.48861031730969745, 0.49632762869199115, 0.4943715234597524, 0.48982996741930646, 0.4938281178474426, 0.48919827739397687, 0.486867219209671, 0.4842636088530223, 0.49763620893160504, 0.49755287170410156, 0.49821798006693524, 0.5028306345144907, 0.4809292455514272, 0.4883957803249359, 0.46985554695129395, 0.47858189543088275, 0.4825609028339386, 0.4949019451936086, 0.47141284743944806, 0.482805867989858, 0.4782057503859202, 0.4866550862789154, 0.4738066792488098, 0.4699362615744273, 0.47626463572184247, 0.47252171238263446, 0.48921040693918866, 0.4608401656150818, 0.4610830048720042, 0.4707030753294627, 0.4640289843082428, 0.4737945298353831, 0.4661054213841756, 0.47239769498507184, 0.47523924708366394, 0.47379793723424274, 0.4777321020762126, 0.4689226845900218, 0.4861786663532257, 0.4632717768351237, 0.4659041265646617, 0.46325353781382245], "test_loss": [0.9298608899116516, 0.9286201000213623, 0.927367091178894, 0.9260708093643188, 0.9246230125427246, 0.9229950904846191, 0.9211652278900146, 0.9191482067108154, 0.9168845415115356, 0.9144287705421448, 0.9117457270622253, 0.9087604284286499, 0.9055784940719604, 0.9021115899085999, 0.8984742164611816, 0.8946555256843567, 0.8906906247138977, 0.8866006731987, 0.8824410438537598, 0.8782087564468384, 0.8738958239555359, 0.8695005774497986, 0.8651221394538879, 0.8607474565505981, 0.8565287590026855, 0.8524606227874756, 0.84834885597229, 0.8445054888725281, 0.8408474922180176, 0.8373579978942871, 0.8339387774467468, 0.8307586312294006, 0.8276570439338684, 0.8246919512748718, 0.8218910694122314, 0.8191938400268555, 0.8164622783660889, 0.814067006111145, 0.8115493655204773, 0.8089558482170105, 0.8065310716629028, 0.8042981624603271, 0.8021987676620483, 0.8000945448875427, 0.7980659008026123, 0.795944094657898, 0.7939944863319397, 0.7918947339057922, 0.7897019386291504, 0.787562370300293, 0.7854297161102295, 0.7832334637641907, 0.7810196876525879, 0.7788534760475159, 0.7766239643096924, 0.7744990587234497, 0.7721480131149292, 0.769902229309082, 0.7678605914115906, 0.7658149003982544, 0.7637181282043457, 0.7617011070251465, 0.7596434950828552, 0.7575833797454834, 0.7554796934127808, 0.7535147070884705, 0.7515978217124939, 0.7497515082359314, 0.7479531764984131, 0.7462489604949951, 0.744451105594635, 0.7426475882530212, 0.7408341765403748, 0.7389852404594421, 0.7371066808700562, 0.735320508480072, 0.7336044907569885, 0.7319872379302979, 0.7304288148880005, 0.728927731513977, 0.7274287343025208, 0.7258942723274231, 0.7242944836616516, 0.7227673530578613, 0.7212544083595276, 0.719752311706543, 0.718218982219696, 0.7167436480522156, 0.7152625322341919, 0.7137777805328369, 0.712334394454956, 0.7109388113021851, 0.7095797061920166, 0.7082014083862305, 0.7068546414375305, 0.7055162191390991, 0.7040970325469971, 0.7026676535606384, 0.7014964818954468, 0.700282633304596, 0.6990247964859009, 0.697831928730011, 0.6965664029121399, 0.6952717304229736, 0.694158136844635, 0.692780613899231, 0.6914969682693481, 0.6900702714920044, 0.6887779235839844, 0.6874729990959167, 0.6863412857055664, 0.6850231885910034, 0.6836751699447632, 0.6825655698776245, 0.6815938353538513, 0.6807154417037964, 0.6797965168952942, 0.678778886795044, 0.6780586242675781, 0.6773285269737244, 0.6765359044075012, 0.6756646037101746, 0.6747848987579346, 0.6738333106040955, 0.6727617383003235, 0.6718459725379944, 0.6710072159767151, 0.6701698303222656, 0.669294536113739, 0.668034017086029, 0.6670897603034973, 0.6662132740020752, 0.6653864979743958, 0.6644272208213806, 0.6636298894882202, 0.6626255512237549, 0.6615933775901794, 0.6606618762016296, 0.6598576307296753, 0.6592516303062439, 0.6586002707481384, 0.6577969193458557, 0.6573529243469238, 0.6567758917808533, 0.6562798619270325, 0.6556363701820374, 0.6548494100570679, 0.6541464924812317, 0.6534116268157959, 0.6525095701217651, 0.6518228054046631, 0.6511184573173523, 0.6504809856414795, 0.6499512791633606, 0.6494218111038208, 0.6489140391349792, 0.6484061479568481, 0.647731363773346, 0.6472024917602539, 0.6465904712677002, 0.6458643674850464, 0.6451480388641357, 0.6448190808296204, 0.6442693471908569, 0.6437927484512329, 0.6435742974281311, 0.6432029008865356, 0.6428396701812744, 0.6423819065093994, 0.6419949531555176, 0.641595721244812, 0.6412363648414612, 0.6409658193588257, 0.640618085861206, 0.640298068523407, 0.6398538947105408, 0.6394216418266296, 0.6389840841293335, 0.638550877571106, 0.6381618976593018, 0.6377515196800232, 0.6371808052062988, 0.6365807056427002, 0.6360141038894653, 0.6352731585502625, 0.6347209215164185, 0.634213387966156, 0.6339948177337646, 0.6337099671363831, 0.6333750486373901, 0.6329785585403442, 0.6326598525047302, 0.6324653625488281, 0.6320960521697998, 0.6318709254264832, 0.6315382719039917, 0.6311991810798645, 0.6310821771621704, 0.6310263276100159, 0.6309400200843811, 0.6306219100952148, 0.6301679611206055, 0.6298125386238098, 0.629535436630249, 0.6295410990715027, 0.6294113397598267, 0.6291530132293701, 0.6289764046669006, 0.6289737224578857, 0.6288029551506042, 0.6284852623939514, 0.628138542175293, 0.6278849840164185, 0.627629280090332, 0.627454936504364, 0.6273459196090698, 0.6272515058517456, 0.6273605823516846, 0.6275609731674194, 0.627708375453949, 0.6276419758796692, 0.6275895833969116, 0.6272011399269104, 0.6267870664596558, 0.6264842748641968, 0.6262391209602356, 0.6260331273078918, 0.6258324980735779, 0.6254996061325073, 0.6251218914985657, 0.6251339316368103, 0.6250628232955933, 0.625011146068573, 0.6249058842658997, 0.6247864961624146, 0.6243884563446045, 0.6242798566818237, 0.6242759823799133, 0.6246921420097351, 0.6247364282608032, 0.6242496371269226, 0.6238921284675598, 0.623569667339325, 0.6234216094017029, 0.6233934164047241, 0.6234481334686279, 0.62331622838974, 0.6231942772865295, 0.6232541799545288, 0.6235364079475403, 0.6235981583595276, 0.6234050989151001, 0.6228485703468323, 0.6221699714660645, 0.6217104196548462, 0.6215617656707764, 0.6215261816978455, 0.6216001510620117, 0.6215519905090332, 0.6216685771942139, 0.6218727827072144, 0.6219251155853271, 0.6217672824859619, 0.6215541362762451, 0.6214060187339783, 0.6216864585876465, 0.621939480304718, 0.621992290019989, 0.6219083666801453, 0.6218449473381042, 0.6216311454772949, 0.6216148138046265, 0.6216328144073486, 0.6220079064369202, 0.6222974061965942, 0.6223437190055847, 0.6223273873329163, 0.6220245361328125, 0.6218318343162537, 0.6212388873100281, 0.6207136511802673, 0.6204036474227905, 0.6206014752388, 0.6209258437156677, 0.6211738586425781, 0.6210983395576477, 0.6211971044540405, 0.6213352084159851, 0.6217947602272034, 0.6220757365226746, 0.6221537590026855, 0.6221935153007507, 0.6221125721931458, 0.6220228672027588, 0.6217535138130188, 0.6214527487754822, 0.6209364533424377, 0.6206910610198975, 0.6206433773040771, 0.6208334565162659]}