-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCodeQwen.py
More file actions
542 lines (454 loc) · 20 KB
/
Copy pathCodeQwen.py
File metadata and controls
542 lines (454 loc) · 20 KB
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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
# CodeQwen1.5-1.8B-Chat
import os
import torch
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
from torch.utils.data import DataLoader
from torch.nn import CrossEntropyLoss
from torch.optim import AdamW
from torch.optim.lr_scheduler import StepLR, CosineAnnealingLR, ExponentialLR
from torch.amp import GradScaler, autocast # Using mixed precision training to reduce memory usage and increase computation speed
# For Code Execution
import ast
import sys
from io import StringIO
import contextlib
# Resource management for code execution
from func_timeout import func_timeout, FunctionTimedOut
import resource
import matplotlib.pyplot as plt
from collections import defaultdict
os.environ['PYTORCH_CUDA_ALLOC_CONF'] = 'max_split_size_mb:128, expandable_segments: True'
def load_data():
"""Load and preprocess the CodeSearchNet dataset."""
# The dataset is available via the Hugging Face Datasets library
# Filter for Python functions only
dataset = load_dataset("claudios/code_search_net", "python", trust_remote_code=True)
print("Dataset loaded:", dataset)
# return dataset
# Use only a small subset of the dataset for experimentation
small_dataset = dataset["train"].train_test_split(test_size = 0.1, shuffle=True)["test"]
return small_dataset
def preprocess_data(dataset, tokenizer):
"""Tokenize and prepare the dataset for training."""
# Save processed data in a format like .jsonl or .pt
def tokenize_function(examples):
# Tokenize the code and documentation strings
max_length = 128 # reduce sequence length during preprocessing to save memory
code_tokens = tokenizer(examples['func_code_string'],
padding="max_length",
truncation=True,
max_length=max_length) # Return PyTorch tensors
doc_tokens = tokenizer(examples['func_documentation_string'],
padding="max_length",
truncation=True,
max_length=max_length) # Return PyTorch tensors
# Use the code tokens as input_ids and the documentation tokens as labels
return {
"input_ids": doc_tokens["input_ids"], # Remove batch dimension
"attention_mask": doc_tokens["attention_mask"],
"labels": code_tokens["input_ids"]
}
tokenized_datasets = dataset.map(tokenize_function, batched=True, batch_size=32)
tokenized_datasets.set_format("torch")
# DEBUGGING LINES
print("Tokenized dataset features:", tokenized_datasets.features)
print("Sample tokenized data:", tokenized_datasets[0])
return tokenized_datasets
def build_model():
"""Initialize CodeQwen model and tokenizer."""
model_name = "Qwen/Qwen1.5-1.8B-Chat"
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
# Use 4-bit quantization for faster inference
quantization_config =BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True
)
# Load model with quantization config
model = AutoModelForCausalLM.from_pretrained(
model_name,
trust_remote_code=True,
device_map="auto",
quantization_config=quantization_config
)
# Check if GPU is available and move model to GPU
# device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# model.to(device)
device = next(model.parameters()).device
print(f"Model loaded on device: {device}")
return model, tokenizer, device
def generate_code(model, tokenizer, prompt, max_length=128):
"""
Generate python code with structured prompt, with explicit attention masks.
Args:
model: CodeQwen1.5-7B-Chat
tokenizer: CodeQwen1.5-7B-Chat tokenizer
max_length = Maximum length of generated sequence
prompt: String prompt for code generation
Returns:
generated_code: String containing the generated Python code
"""
# Format prompt according to CodeQwen chat template
formatted_prompt = f"<|im_start|>user\nWrite a Python function that {prompt}. Only include the code with no additional explanation or examples.<|im_end|>\n<|im_start|>assistant\n"
# Tokenize the prompt
encoded_input = tokenizer(
formatted_prompt,
return_tensors="pt",
padding=True,
truncation=True,
max_length=max_length
).to(model.device)
# Generate with appropriate parameters for code
with torch.no_grad():
outputs = model.generate(
**encoded_input,
max_new_tokens=max_length,
temperature=0.2,
top_p=0.95,
top_k=40,
repetition_penalty=1.1,
do_sample=True,
pad_token_id=tokenizer.pad_token_id if tokenizer.pad_token_id is not None else tokenizer.eos_token_id
)
# Improve code extraction (issue: older output includes instruction text)
generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
if "def " in generated_text:
# Get function start
code_start = generated_text.find("def ")
function_code = generated_text[code_start:]
# Find function end by detecting indent pattern
lines = function_code.split('\n')
function_lines = [lines[0]] # First line with 'def'
indent_level = None
# Process line by line to find where function ends
for i, line in enumerate(lines[1:], 1):
# First indented line determines indent level
if indent_level is None and line.strip():
indent_level = len(line) - len(line.lstrip())
# Add lines that belong to function (indented or blank)
if not line.strip() or len(line) - len(line.lstrip()) >= indent_level:
function_lines.append(line)
else:
# Non-indented, non-empty line means function ended
break
# Join only the function lines
generated_code = '\n'.join(function_lines)
# Ensure proper indentation
lines = generated_code.split('\n')
if len(lines) > 1:
base_indent = len(lines[1]) - len(lines[1].lstrip())
formatted_lines = [lines[0]] # Keep first line as is
for line in lines[1:]:
if line.strip(): # Skip empty lines
formatted_lines.append(" " * 4 + line[base_indent:])
generated_code = '\n'.join(formatted_lines)
else:
generated_code = "def " + generated_text.split("def", 1)[1] if "def" in generated_text else generated_text
return generated_code
def execute(code_str, test_input=None, timeout=2):
"""
Safely execute generated code with test input, and capture its output.
Args:
code_str: String containing Python code to execute.
Returns:
success: Boolean indicating whether the code executed successfully.
output: String containing execution output or error message.
"""
# Add resource limits
# resource.setrlimit(resource.RLIMIT_AS, (256 * 1024 * 1024, -1)) # GPU
# resource.setrlimit(resource.RLIMIT_CPU, (2, -1)) # 1 second CPU time
# Create string buffer to capture output
output_buffer = StringIO()
try:
# First, validate Python syntax
ast.parse(code_str) # ast is the Abstract Syntax Tree module in Python
# Create execution environment
global_env = {"__builtins__": __builtins__}
local_env = {}
def execute_code():
# Redirect stdout to capture output
with contextlib.redirect_stdout(output_buffer):
# Execute the code in a safe environment
exec(code_str, global_env, local_env)
# Execute test input if provided
if test_input:
exec(test_input, global_env, local_env)
# Execute with timeout
func_timeout(timeout, execute_code)
return True, output_buffer.getvalue()
except FunctionTimedOut:
return False, "Execution timed out"
except SyntaxError as e:
return False, f"Syntax Error: {str(e)}"
except Exception as e:
return False, f"Runtime Error: {str(e)}"
finally:
output_buffer.close()
def evaluate_code(model, tokenizer, test_cases):
"""
Evaluate the model's code generation capabilities.
Args:
model: CodeQwen1.5-1.8B-Chat
tokenizer: CodeQwen1.5-1.8B-Chat tokenizer
test_cases: List of dictionaries containing:
- 'func_documentation_string': Documentation for the function
- 'expected_output': Expected output
- 'test_input': Test input to execute the generated code
Returns:
metrics: Dictionary containing evaluation metrics
"""
results = {
'total_cases': len(test_cases),
'syntax_correct': 0,
'execution_successful': 0,
'output_matches': 0,
'test_results': []
}
for test_case in test_cases:
# Generate code
generated_code = generate_code(model, tokenizer, test_case['func_documentation_string'])
# Execute the generated code, using the test input
success, output = execute(generated_code, test_case['test_input'])
# Create individual test result
test_result = {
'generated_code': generated_code,
'success': success,
'output': output,
'output_matches': False
}
# Update metrics
if success:
results['syntax_correct'] += 1
results['execution_successful'] += 1
# Compare output with expected output
if output.strip() == test_case['expected_output'].strip():
results['output_matches'] += 1
# Add to test results
results['test_results'].append(test_result)
print(f"\nPrompt: {test_case['func_documentation_string']}")
print(f"Generated Code:\n{generated_code}")
print(f"Execution success: {success}")
print(f"Output: {output}")
# Calculate percentages
total = results['total_cases']
results['syntax_accuracy'] = results['syntax_correct'] / total * 100
results['execution_accuracy'] = results['execution_successful'] / total * 100
results['functional_accuracy'] = results['output_matches'] / total * 100
return results
def train_model(model, train_dataset, valid_dataset):
"""Train the model on the dataset."""
train_dataloader = DataLoader(train_dataset, batch_size=2, shuffle=True)
valid_dataloader = DataLoader(valid_dataset, batch_size=2, shuffle=True)
history = {
'train_loss': [],
'valid_loss': []
}
print(f"Using device: {model.device}") # DEBUGGING LINE
# DEBUGGING LINES
# try:
# sample_batch = next(iter(train_dataloader))
# if torch.is_tensor(sample_batch['input_ids']):
# print(f"Sample batch device: {sample_batch['input_ids'].device}")
# else:
# print("Warning: input_ids is not a tensor")
# except Exception as e:
# print(f"Debug info error: {e}")
optimizer = AdamW(
model.parameters(),
lr=5e-5,
weight_decay=0.01,
eps=1e-8
) #Add L2 regularization/weight decay to prevent overfitting
gradient_accumulation_steps = 8
num_epochs = 5
num_training_steps = len(train_dataloader) * num_epochs # 20 epochs
num_warmup_steps = num_training_steps // 20
scheduler = ExponentialLR(
optimizer,
gamma=0.7 # Decay factor (after each epoch)
)
current_lr = scheduler.get_last_lr()[0] # Get initial learning rate
criterion = CrossEntropyLoss() # Initialize the loss function
print("Training process started...")
for epoch in range(num_epochs):
model.train()
total_train_loss = 0
num_batches = len(train_dataloader)
for i, batch in enumerate(train_dataloader):
inputs = batch['input_ids'].to(model.device) # Move input data to GPU (tokenized code inputs)
labels = batch['labels'].to(model.device) # Move labels to GPU (expected outputs/targets)
# outputs = model(inputs, labels=labels) # forward pass
# loss = criterion(outputs.logits.view(-1, outputs.logits.size(-1)), labels.view(-1)) # Calculate loss from model outputs
# loss.backward() # Calculate gradients with backpropagation
# optimizer.step() # Update model weights
outputs = model(inputs, labels = labels)
loss = outputs.loss
# Only update after accumulating gradients
if (i+1) % gradient_accumulation_steps == 0:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) # Gradient clipping (max L2 norm to 5.0)
optimizer.step()
optimizer.zero_grad(set_to_none=True) # Reset gradients to zero to avoid accumulation
total_train_loss += loss.item()
if i % 100 == 0:
print(f"Epoch {epoch+1}, Batch {i}, Loss: {loss.item()}, Learning Rate: {current_lr}")
torch.cuda.empty_cache() # Clear GPU cache every 100 batches
scheduler.step() # Performs optimizer step with scaled gradients
# Learning rate logging
current_lr = scheduler.get_last_lr()[0]
# Print epoch average
avg_train_loss = total_train_loss / num_batches
history['train_loss'].append(avg_train_loss)
# DEBUGGING LINES
# print("Training dataset keys:", train_dataset.features)
# print("First batch keys:", next(iter(DataLoader(train_dataset, batch_size=1))))
# Validation after each epoch
model.eval()
total_valid_loss = 0
with torch.no_grad():
for batch in valid_dataloader:
inputs = batch['input_ids'].to(model.device)
labels = batch['labels'].to(model.device)
outputs = model(inputs, labels=labels)
total_valid_loss += outputs.loss.item()
avg_valid_loss = total_valid_loss / len(valid_dataloader)
history['valid_loss'].append(avg_valid_loss)
print(f"Epoch {epoch+1}, Train_loss:{avg_train_loss:.4f}, Valid_loss {avg_valid_loss:.4f}, Learning Rate: {current_lr}")
return history
# Evaluation
def test_model(model, test_dataset):
"""Evaluate on test data."""
test_dataloader = DataLoader(test_dataset, batch_size=8)
total_test_loss = 0
num_batches = len(test_dataloader)
model.eval()
print("Testing process started...")
with torch.no_grad():
for i, batch in enumerate(test_dataloader):
inputs = batch["input_ids"].to(model.device)
labels = batch["labels"].to(model.device)
outputs = model(inputs, labels=labels)
total_test_loss += outputs.loss.item()
if i % 100 == 0:
print(f"Batch {i}, Loss: {outputs.loss.item()}")
avg_test_loss = total_test_loss / num_batches
print(f"Average Test Loss: {avg_test_loss:.4f}")
return avg_test_loss
def plot_learning_curves(history):
"""Plot training vs. validation learning curves"""
plt.figure(figsize=(10, 6))
epochs = range(1, len(history['train_loss']) + 1) # integer x-axis values
plt.plot(epochs, history['train_loss'], label='Training Loss')
plt.plot(epochs, history['valid_loss'], label='Validation Loss')
plt.title('Learning Curves')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.grid(True)
plt.xticks(epochs) # Force integer ticks on x-axis
plt.savefig('Learning_Curves_Qwen.png')
plt.close()
def save_model(model, tokenizer, path="./model_checkpoint_qwen"):
"""Save the trained model."""
os.makedirs(path, exist_ok=True)
# Save the quantized model
model.save_pretrained(
path,
safe_serialization=True # Use safetensors format
)
# Save the tokenizer
tokenizer.save_pretrained(path)
print(f"Model saved to {path}")
def load_model(path="./model_checkpoint_qwen"):
"""Load a saved model and tokenizer."""
model = AutoModelForCausalLM.from_pretrained(
path,
trust_remote_code=True,
device_map="auto",
quantization_config=BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4big_quant_type="nf4",
bnb_4bit_use_double_quant=True
)
)
# Load the tokenizer
tokenizer = AutoTokenizer.from_pretrained(path, trust_remote_code=True)
print(f"Model and tokenizer loaded from {path}")
return model, tokenizer
def main():
dataset = load_data()
print("Original dataset features:", dataset.features) # DEBUGGING LINE
model, tokenizer, device = build_model()
tokenized_dataset = preprocess_data(dataset, tokenizer)
print("Tokenized dataset features:", tokenized_dataset.features) # DEBUGGING LINE
# split_dataset = dataset["train"].train_test_split(test_size=0.2, shuffle=True)
# Using the following line for small_dataset for experimentation
split_dataset = tokenized_dataset.train_test_split(test_size=0.2, shuffle=True)
split_temp = split_dataset["train"].train_test_split(test_size=0.5, shuffle=True)
train_dataset = split_dataset["train"]
valid_dataset = split_temp["train"]
test_dataset = split_temp["test"]
print(torch.cuda.is_available()) # Should return True if GPU is available
print(torch.cuda.device_count()) # Number of available GPUs
print(torch.cuda.get_device_name(0)) # GPU name
history = train_model(model, train_dataset, valid_dataset)
test_model(model, test_dataset)
save_model(model, tokenizer)
# Generate Learning curves
plot_learning_curves(history)
# The following are temporary & for experimentation. Need to be replaced.
# Define test cases for evaluation
test_cases = [
{
'func_documentation_string': 'Add two numbers a and b',
'expected_output': '12',
'test_input': 'print(add(5,7))'
},
{
'func_documentation_string': 'Calculate factorial of a number n',
'expected_output': '120',
'test_input': 'print(factorial(5))'
},
{
'func_documentation_string': 'Check if a number is prime',
'expected_output': 'True',
'test_input': 'print(is_prime(17))'
},
{
'func_documentation_string': 'Reverse a string',
'expected_output': 'olleh',
'test_input': 'print(reverse_string("hello"))'
},
{
'func_documentation_string': 'Find the maximum value in a list of numbers',
'expected_output': '42',
'test_input': 'print(find_max([5, 42, 17, 8, 1]))'
},
{
'func_documentation_string': 'Count the number of vowels in a string',
'expected_output': '2',
'test_input': 'print(count_vowels("hello"))'
},
{
'func_documentation_string': 'Generate Fibonacci sequence up to n terms',
'expected_output': '[0, 1, 1, 2, 3]',
'test_input': 'print(fibonacci(5))'
},
{
'func_documentation_string': 'Convert temperature from Celsius to Fahrenheit',
'expected_output': '98.6',
'test_input': 'print(celsius_to_fahrenheit(37))'
}
]
# Evaluate the model
print("\nEvaluating model...")
evaluation_results = evaluate_code(model, tokenizer, test_cases)
# Print evaluation results
print("\nEvaluation Results:")
print(f"Syntax Accuracy: {evaluation_results['syntax_accuracy']:.2f}%")
print(f"Execution Accuracy: {evaluation_results['execution_accuracy']:.2f}%")
print(f"Functional Accuracy: {evaluation_results['functional_accuracy']:.2f}%")
if __name__ == "__main__":
main()