fine 0.1.0
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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +38 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +167 -0
- data/LICENSE +21 -0
- data/README.md +212 -0
- data/Rakefile +6 -0
- data/docs/installation.md +151 -0
- data/docs/tutorials/llm-fine-tuning.md +246 -0
- data/docs/tutorials/model-export.md +200 -0
- data/docs/tutorials/siglip2-image-classification.md +130 -0
- data/docs/tutorials/siglip2-object-recognition.md +203 -0
- data/docs/tutorials/siglip2-similarity-search.md +152 -0
- data/docs/tutorials/text-classification.md +233 -0
- data/docs/tutorials/text-embeddings.md +211 -0
- data/examples/basic_classification.rb +70 -0
- data/examples/data/tool_calls.jsonl +30 -0
- data/examples/demo_training.rb +78 -0
- data/examples/finetune_gemma3_tools.rb +135 -0
- data/examples/real_llm_test.rb +128 -0
- data/examples/real_text_classification_test.rb +90 -0
- data/examples/real_text_embedder_test.rb +110 -0
- data/examples/real_training_test.rb +88 -0
- data/examples/test_export.rb +28 -0
- data/examples/test_image_classifier.rb +79 -0
- data/examples/test_llm.rb +100 -0
- data/examples/test_text_classifier.rb +59 -0
- data/lib/fine/callbacks/base.rb +140 -0
- data/lib/fine/callbacks/progress_bar.rb +66 -0
- data/lib/fine/configuration.rb +106 -0
- data/lib/fine/datasets/data_loader.rb +63 -0
- data/lib/fine/datasets/image_dataset.rb +203 -0
- data/lib/fine/datasets/instruction_dataset.rb +226 -0
- data/lib/fine/datasets/text_data_loader.rb +88 -0
- data/lib/fine/datasets/text_dataset.rb +266 -0
- data/lib/fine/error.rb +49 -0
- data/lib/fine/export/gguf_exporter.rb +424 -0
- data/lib/fine/export/onnx_exporter.rb +249 -0
- data/lib/fine/export.rb +53 -0
- data/lib/fine/hub/config_loader.rb +145 -0
- data/lib/fine/hub/model_downloader.rb +136 -0
- data/lib/fine/hub/safetensors_loader.rb +108 -0
- data/lib/fine/image_classifier.rb +256 -0
- data/lib/fine/llm.rb +336 -0
- data/lib/fine/models/base.rb +48 -0
- data/lib/fine/models/bert_encoder.rb +202 -0
- data/lib/fine/models/bert_for_sequence_classification.rb +226 -0
- data/lib/fine/models/causal_lm.rb +279 -0
- data/lib/fine/models/classification_head.rb +24 -0
- data/lib/fine/models/gemma3_decoder.rb +244 -0
- data/lib/fine/models/llama_decoder.rb +297 -0
- data/lib/fine/models/sentence_transformer.rb +202 -0
- data/lib/fine/models/siglip2_for_image_classification.rb +155 -0
- data/lib/fine/models/siglip2_vision_encoder.rb +190 -0
- data/lib/fine/text_classifier.rb +250 -0
- data/lib/fine/text_embedder.rb +221 -0
- data/lib/fine/tokenizers/auto_tokenizer.rb +208 -0
- data/lib/fine/training/llm_trainer.rb +212 -0
- data/lib/fine/training/text_trainer.rb +275 -0
- data/lib/fine/training/trainer.rb +194 -0
- data/lib/fine/transforms/compose.rb +28 -0
- data/lib/fine/transforms/normalize.rb +33 -0
- data/lib/fine/transforms/resize.rb +35 -0
- data/lib/fine/transforms/to_tensor.rb +53 -0
- data/lib/fine/version.rb +3 -0
- data/lib/fine.rb +112 -0
- data/mise.toml +2 -0
- metadata +240 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Fine-tuning Text Embedding Models
|
|
2
|
+
|
|
3
|
+
Train text embeddings for semantic search, clustering, and similarity matching.
|
|
4
|
+
|
|
5
|
+
## When to Use This
|
|
6
|
+
|
|
7
|
+
- Build semantic search for your domain (legal docs, medical records, code)
|
|
8
|
+
- Improve retrieval for RAG applications
|
|
9
|
+
- Cluster similar documents
|
|
10
|
+
- Match queries to FAQ answers
|
|
11
|
+
- Detect duplicate content
|
|
12
|
+
|
|
13
|
+
## Supported Models
|
|
14
|
+
|
|
15
|
+
| Model | Size | Use Case |
|
|
16
|
+
|-------|------|----------|
|
|
17
|
+
| `sentence-transformers/all-MiniLM-L6-v2` | 22M | Fast, general purpose |
|
|
18
|
+
| `sentence-transformers/all-mpnet-base-v2` | 110M | Better quality |
|
|
19
|
+
| `BAAI/bge-small-en-v1.5` | 33M | Retrieval optimized |
|
|
20
|
+
| `BAAI/bge-base-en-v1.5` | 110M | Best retrieval quality |
|
|
21
|
+
|
|
22
|
+
## Dataset Format
|
|
23
|
+
|
|
24
|
+
Prepare your data as pairs or triplets:
|
|
25
|
+
|
|
26
|
+
### Pairs (query, positive)
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
data/pairs.jsonl
|
|
30
|
+
{"query": "How do I reset my password?", "positive": "To reset your password, click 'Forgot Password' on the login page."}
|
|
31
|
+
{"query": "What's your return policy?", "positive": "We accept returns within 30 days of purchase."}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Triplets (query, positive, negative)
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
data/triplets.jsonl
|
|
38
|
+
{"query": "python list append", "positive": "list.append(x) adds item x to the end", "negative": "list.pop() removes the last item"}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Basic Training
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
require "fine"
|
|
45
|
+
|
|
46
|
+
embedder = Fine::TextEmbedder.new("sentence-transformers/all-MiniLM-L6-v2")
|
|
47
|
+
|
|
48
|
+
embedder.fit(
|
|
49
|
+
train_file: "data/pairs.jsonl",
|
|
50
|
+
epochs: 3
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
embedder.save("models/my_embedder")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Training for Semantic Search
|
|
57
|
+
|
|
58
|
+
Optimize for retrieval with contrastive loss:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
embedder = Fine::TextEmbedder.new("BAAI/bge-base-en-v1.5") do |config|
|
|
62
|
+
config.epochs = 5
|
|
63
|
+
config.batch_size = 32
|
|
64
|
+
config.learning_rate = 2e-5
|
|
65
|
+
config.loss = :multiple_negatives_ranking # Best for retrieval
|
|
66
|
+
|
|
67
|
+
config.on_epoch_end do |epoch, metrics|
|
|
68
|
+
puts "Epoch #{epoch}: loss=#{metrics[:loss]}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
embedder.fit(train_file: "data/queries_documents.jsonl")
|
|
73
|
+
embedder.save("models/search_embedder")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Generating Embeddings
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
embedder = Fine::TextEmbedder.load("models/my_embedder")
|
|
80
|
+
|
|
81
|
+
# Single text
|
|
82
|
+
embedding = embedder.encode("How do I cancel my subscription?")
|
|
83
|
+
# => Array of 384 floats (for MiniLM)
|
|
84
|
+
|
|
85
|
+
# Batch encoding
|
|
86
|
+
embeddings = embedder.encode([
|
|
87
|
+
"First document",
|
|
88
|
+
"Second document",
|
|
89
|
+
"Third document"
|
|
90
|
+
])
|
|
91
|
+
# => Array of 3 embeddings
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Building a Search Index
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
class SemanticSearch
|
|
98
|
+
def initialize(embedder)
|
|
99
|
+
@embedder = embedder
|
|
100
|
+
@documents = []
|
|
101
|
+
@embeddings = []
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def add(text, metadata = {})
|
|
105
|
+
@documents << { text: text, metadata: metadata }
|
|
106
|
+
@embeddings << @embedder.encode(text)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def search(query, top_k: 5)
|
|
110
|
+
query_emb = @embedder.encode(query)
|
|
111
|
+
|
|
112
|
+
scores = @embeddings.map { |emb| cosine_similarity(query_emb, emb) }
|
|
113
|
+
|
|
114
|
+
scores
|
|
115
|
+
.each_with_index
|
|
116
|
+
.sort_by { |score, _| -score }
|
|
117
|
+
.first(top_k)
|
|
118
|
+
.map { |score, idx| { document: @documents[idx], score: score } }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def cosine_similarity(a, b)
|
|
124
|
+
dot = a.zip(b).sum { |x, y| x * y }
|
|
125
|
+
norm_a = Math.sqrt(a.sum { |x| x * x })
|
|
126
|
+
norm_b = Math.sqrt(b.sum { |x| x * x })
|
|
127
|
+
dot / (norm_a * norm_b)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Usage
|
|
132
|
+
search = SemanticSearch.new(embedder)
|
|
133
|
+
|
|
134
|
+
# Index your documents
|
|
135
|
+
documents.each { |doc| search.add(doc[:text], id: doc[:id]) }
|
|
136
|
+
|
|
137
|
+
# Search
|
|
138
|
+
results = search.search("how to get a refund")
|
|
139
|
+
results.each do |r|
|
|
140
|
+
puts "#{r[:score].round(3)}: #{r[:document][:text][0..50]}..."
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Domain Adaptation
|
|
145
|
+
|
|
146
|
+
Fine-tune on your specific domain for better results:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
# Prepare domain-specific pairs
|
|
150
|
+
# Example: customer support queries matched to answers
|
|
151
|
+
pairs = [
|
|
152
|
+
{ query: "broken screen", positive: "Screen repair costs $99 and takes 2-3 days" },
|
|
153
|
+
{ query: "cracked display", positive: "Screen repair costs $99 and takes 2-3 days" },
|
|
154
|
+
# ... more domain-specific examples
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
# Save as JSONL
|
|
158
|
+
File.write("data/support_pairs.jsonl", pairs.map(&:to_json).join("\n"))
|
|
159
|
+
|
|
160
|
+
# Fine-tune
|
|
161
|
+
embedder = Fine::TextEmbedder.new("sentence-transformers/all-mpnet-base-v2")
|
|
162
|
+
embedder.fit(train_file: "data/support_pairs.jsonl", epochs: 3)
|
|
163
|
+
embedder.save("models/support_search")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Hard Negatives Mining
|
|
167
|
+
|
|
168
|
+
For better quality, use hard negatives (similar but wrong answers):
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
embedder = Fine::TextEmbedder.new("BAAI/bge-base-en-v1.5") do |config|
|
|
172
|
+
config.loss = :triplet
|
|
173
|
+
config.margin = 0.5 # Minimum distance between positive and negative
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Triplet format
|
|
177
|
+
embedder.fit(train_file: "data/triplets.jsonl")
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Evaluation
|
|
181
|
+
|
|
182
|
+
Test retrieval quality:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
def evaluate_retrieval(embedder, test_queries, ground_truth)
|
|
186
|
+
hits_at_1 = 0
|
|
187
|
+
hits_at_5 = 0
|
|
188
|
+
|
|
189
|
+
test_queries.each do |query, expected_doc_id|
|
|
190
|
+
results = search.search(query, top_k: 5)
|
|
191
|
+
|
|
192
|
+
result_ids = results.map { |r| r[:document][:metadata][:id] }
|
|
193
|
+
|
|
194
|
+
hits_at_1 += 1 if result_ids[0] == expected_doc_id
|
|
195
|
+
hits_at_5 += 1 if result_ids.include?(expected_doc_id)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
{
|
|
199
|
+
recall_at_1: hits_at_1.to_f / test_queries.size,
|
|
200
|
+
recall_at_5: hits_at_5.to_f / test_queries.size
|
|
201
|
+
}
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Best Practices
|
|
206
|
+
|
|
207
|
+
1. **Start with a good base model** - BGE models work best for retrieval
|
|
208
|
+
2. **Use domain-specific data** - Even 1000 pairs helps significantly
|
|
209
|
+
3. **Include hard negatives** - Similar but incorrect matches improve precision
|
|
210
|
+
4. **Evaluate on held-out queries** - Don't overfit to training data
|
|
211
|
+
5. **Consider asymmetric models** - Some models use different encodings for queries vs documents
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Basic image classification example
|
|
5
|
+
#
|
|
6
|
+
# This example shows how to fine-tune a SigLIP2 model for image classification.
|
|
7
|
+
#
|
|
8
|
+
# Dataset structure expected:
|
|
9
|
+
# data/
|
|
10
|
+
# train/
|
|
11
|
+
# cat/
|
|
12
|
+
# cat1.jpg
|
|
13
|
+
# cat2.jpg
|
|
14
|
+
# dog/
|
|
15
|
+
# dog1.jpg
|
|
16
|
+
# dog2.jpg
|
|
17
|
+
# val/
|
|
18
|
+
# cat/
|
|
19
|
+
# cat3.jpg
|
|
20
|
+
# dog/
|
|
21
|
+
# dog3.jpg
|
|
22
|
+
|
|
23
|
+
require "fine"
|
|
24
|
+
|
|
25
|
+
# Configure Fine (optional)
|
|
26
|
+
Fine.configure do |config|
|
|
27
|
+
config.cache_dir = File.expand_path("~/.cache/fine")
|
|
28
|
+
config.progress_bar = true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Create a classifier
|
|
32
|
+
classifier = Fine::ImageClassifier.new("google/siglip2-base-patch16-224") do |config|
|
|
33
|
+
# Training settings
|
|
34
|
+
config.epochs = 3
|
|
35
|
+
config.batch_size = 8
|
|
36
|
+
config.learning_rate = 2e-4
|
|
37
|
+
|
|
38
|
+
# Model settings
|
|
39
|
+
config.freeze_encoder = false # Full fine-tuning
|
|
40
|
+
config.dropout = 0.1
|
|
41
|
+
|
|
42
|
+
# Callbacks
|
|
43
|
+
config.on_epoch_end do |epoch, metrics|
|
|
44
|
+
puts "Epoch #{epoch + 1}:"
|
|
45
|
+
puts " Train Loss: #{metrics[:loss].round(4)}"
|
|
46
|
+
puts " Train Acc: #{(metrics[:accuracy] * 100).round(2)}%"
|
|
47
|
+
if metrics[:val_loss]
|
|
48
|
+
puts " Val Loss: #{metrics[:val_loss].round(4)}"
|
|
49
|
+
puts " Val Acc: #{(metrics[:val_accuracy] * 100).round(2)}%"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Train the model
|
|
55
|
+
puts "Starting training..."
|
|
56
|
+
history = classifier.fit(
|
|
57
|
+
train_dir: "data/train",
|
|
58
|
+
val_dir: "data/val"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Save the model
|
|
62
|
+
classifier.save("models/my_classifier")
|
|
63
|
+
puts "Model saved to models/my_classifier"
|
|
64
|
+
|
|
65
|
+
# Make predictions
|
|
66
|
+
puts "\nMaking predictions..."
|
|
67
|
+
predictions = classifier.predict("data/test/image.jpg")
|
|
68
|
+
predictions.each do |pred|
|
|
69
|
+
puts " #{pred[:label]}: #{(pred[:score] * 100).round(2)}%"
|
|
70
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{"instruction": "What's the weather in Tokyo?", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"get_weather\", \"description\": \"Get current weather for a location\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\", \"description\": \"City name\"}}, \"required\": [\"location\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Tokyo\"}}]"}
|
|
2
|
+
{"instruction": "Calculate 25 * 4 + 10", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"calculate\", \"description\": \"Evaluate a math expression\", \"parameters\": {\"type\": \"object\", \"properties\": {\"expression\": {\"type\": \"string\", \"description\": \"Math expression\"}}, \"required\": [\"expression\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"calculate\", \"arguments\": {\"expression\": \"25 * 4 + 10\"}}]"}
|
|
3
|
+
{"instruction": "Search for the latest news about AI", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"search_web\", \"description\": \"Search the web\", \"parameters\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\", \"description\": \"Search query\"}}, \"required\": [\"query\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"search_web\", \"arguments\": {\"query\": \"latest news about AI\"}}]"}
|
|
4
|
+
{"instruction": "What's the temperature in New York City?", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"get_weather\", \"description\": \"Get current weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\"}}, \"required\": [\"location\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"get_weather\", \"arguments\": {\"location\": \"New York City\"}}]"}
|
|
5
|
+
{"instruction": "How much is 15% of 200?", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"calculate\", \"description\": \"Evaluate math\", \"parameters\": {\"type\": \"object\", \"properties\": {\"expression\": {\"type\": \"string\"}}, \"required\": [\"expression\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"calculate\", \"arguments\": {\"expression\": \"0.15 * 200\"}}]"}
|
|
6
|
+
{"instruction": "Find information about Ruby programming", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"search_web\", \"description\": \"Search the web\", \"parameters\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}, \"required\": [\"query\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"search_web\", \"arguments\": {\"query\": \"Ruby programming language\"}}]"}
|
|
7
|
+
{"instruction": "Check weather in London", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"get_weather\", \"description\": \"Get weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\"}}, \"required\": [\"location\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"get_weather\", \"arguments\": {\"location\": \"London\"}}]"}
|
|
8
|
+
{"instruction": "What is sqrt(144)?", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"calculate\", \"description\": \"Evaluate math\", \"parameters\": {\"type\": \"object\", \"properties\": {\"expression\": {\"type\": \"string\"}}, \"required\": [\"expression\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"calculate\", \"arguments\": {\"expression\": \"sqrt(144)\"}}]"}
|
|
9
|
+
{"instruction": "Look up population of Japan", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"search_web\", \"description\": \"Search web\", \"parameters\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}, \"required\": [\"query\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"search_web\", \"arguments\": {\"query\": \"population of Japan\"}}]"}
|
|
10
|
+
{"instruction": "Is it raining in Seattle?", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"get_weather\", \"description\": \"Get weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\"}}, \"required\": [\"location\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Seattle\"}}]"}
|
|
11
|
+
{"instruction": "Divide 1000 by 8", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"calculate\", \"description\": \"Math\", \"parameters\": {\"type\": \"object\", \"properties\": {\"expression\": {\"type\": \"string\"}}, \"required\": [\"expression\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"calculate\", \"arguments\": {\"expression\": \"1000 / 8\"}}]"}
|
|
12
|
+
{"instruction": "Search machine learning tutorials", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"search_web\", \"description\": \"Search\", \"parameters\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}, \"required\": [\"query\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"search_web\", \"arguments\": {\"query\": \"machine learning tutorials\"}}]"}
|
|
13
|
+
{"instruction": "Weather in Paris today", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"get_weather\", \"description\": \"Weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\"}}, \"required\": [\"location\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Paris\"}}]"}
|
|
14
|
+
{"instruction": "2 to the power of 10", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"calculate\", \"description\": \"Math\", \"parameters\": {\"type\": \"object\", \"properties\": {\"expression\": {\"type\": \"string\"}}, \"required\": [\"expression\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"calculate\", \"arguments\": {\"expression\": \"2 ** 10\"}}]"}
|
|
15
|
+
{"instruction": "Send email to john@example.com about the meeting tomorrow", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"send_email\", \"description\": \"Send an email\", \"parameters\": {\"type\": \"object\", \"properties\": {\"to\": {\"type\": \"string\"}, \"subject\": {\"type\": \"string\"}, \"body\": {\"type\": \"string\"}}, \"required\": [\"to\", \"subject\", \"body\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"send_email\", \"arguments\": {\"to\": \"john@example.com\", \"subject\": \"Meeting Tomorrow\", \"body\": \"Hi, I wanted to discuss the meeting scheduled for tomorrow.\"}}]"}
|
|
16
|
+
{"instruction": "Create event for Friday at 3pm called Team Standup", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"create_event\", \"description\": \"Create calendar event\", \"parameters\": {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}, \"date\": {\"type\": \"string\"}, \"time\": {\"type\": \"string\"}}, \"required\": [\"title\", \"date\", \"time\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"create_event\", \"arguments\": {\"title\": \"Team Standup\", \"date\": \"Friday\", \"time\": \"3pm\"}}]"}
|
|
17
|
+
{"instruction": "Set reminder to buy milk", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"set_reminder\", \"description\": \"Set a reminder\", \"parameters\": {\"type\": \"object\", \"properties\": {\"message\": {\"type\": \"string\"}}, \"required\": [\"message\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"set_reminder\", \"arguments\": {\"message\": \"buy milk\"}}]"}
|
|
18
|
+
{"instruction": "Translate hello to Spanish", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"translate\", \"description\": \"Translate text\", \"parameters\": {\"type\": \"object\", \"properties\": {\"text\": {\"type\": \"string\"}, \"target_language\": {\"type\": \"string\"}}, \"required\": [\"text\", \"target_language\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"translate\", \"arguments\": {\"text\": \"hello\", \"target_language\": \"Spanish\"}}]"}
|
|
19
|
+
{"instruction": "Play jazz music", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"play_music\", \"description\": \"Play music\", \"parameters\": {\"type\": \"object\", \"properties\": {\"genre\": {\"type\": \"string\"}}, \"required\": [\"genre\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"play_music\", \"arguments\": {\"genre\": \"jazz\"}}]"}
|
|
20
|
+
{"instruction": "Turn off living room lights", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"control_lights\", \"description\": \"Control smart lights\", \"parameters\": {\"type\": \"object\", \"properties\": {\"room\": {\"type\": \"string\"}, \"action\": {\"type\": \"string\", \"enum\": [\"on\", \"off\"]}}, \"required\": [\"room\", \"action\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"control_lights\", \"arguments\": {\"room\": \"living room\", \"action\": \"off\"}}]"}
|
|
21
|
+
{"instruction": "What time is it in Sydney?", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"get_time\", \"description\": \"Get time in timezone\", \"parameters\": {\"type\": \"object\", \"properties\": {\"timezone\": {\"type\": \"string\"}}, \"required\": [\"timezone\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"get_time\", \"arguments\": {\"timezone\": \"Australia/Sydney\"}}]"}
|
|
22
|
+
{"instruction": "Book flight to Miami next Friday", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"book_flight\", \"description\": \"Book a flight\", \"parameters\": {\"type\": \"object\", \"properties\": {\"destination\": {\"type\": \"string\"}, \"date\": {\"type\": \"string\"}}, \"required\": [\"destination\", \"date\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"book_flight\", \"arguments\": {\"destination\": \"Miami\", \"date\": \"next Friday\"}}]"}
|
|
23
|
+
{"instruction": "Order large pepperoni pizza", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"order_food\", \"description\": \"Order food\", \"parameters\": {\"type\": \"object\", \"properties\": {\"item\": {\"type\": \"string\"}, \"size\": {\"type\": \"string\"}, \"toppings\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}}}, \"required\": [\"item\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"order_food\", \"arguments\": {\"item\": \"pizza\", \"size\": \"large\", \"toppings\": [\"pepperoni\"]}}]"}
|
|
24
|
+
{"instruction": "Check my bank balance", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"get_balance\", \"description\": \"Get account balance\", \"parameters\": {\"type\": \"object\", \"properties\": {\"account_type\": {\"type\": \"string\", \"enum\": [\"checking\", \"savings\"]}}, \"required\": [\"account_type\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"get_balance\", \"arguments\": {\"account_type\": \"checking\"}}]"}
|
|
25
|
+
{"instruction": "Get weather in Berlin and calculate 100/4", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"get_weather\", \"description\": \"Get weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\"}}, \"required\": [\"location\"]}}}, {\"type\": \"function\", \"function\": {\"name\": \"calculate\", \"description\": \"Math\", \"parameters\": {\"type\": \"object\", \"properties\": {\"expression\": {\"type\": \"string\"}}, \"required\": [\"expression\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Berlin\"}}, {\"name\": \"calculate\", \"arguments\": {\"expression\": \"100/4\"}}]"}
|
|
26
|
+
{"instruction": "Search for restaurants and get weather in Chicago", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"search_web\", \"description\": \"Search\", \"parameters\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}, \"required\": [\"query\"]}}}, {\"type\": \"function\", \"function\": {\"name\": \"get_weather\", \"description\": \"Weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\"}}, \"required\": [\"location\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"search_web\", \"arguments\": {\"query\": \"restaurants\"}}, {\"name\": \"get_weather\", \"arguments\": {\"location\": \"Chicago\"}}]"}
|
|
27
|
+
{"instruction": "Read file config.json", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"read_file\", \"description\": \"Read a file\", \"parameters\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}}, \"required\": [\"path\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"read_file\", \"arguments\": {\"path\": \"config.json\"}}]"}
|
|
28
|
+
{"instruction": "Write 'hello world' to output.txt", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"write_file\", \"description\": \"Write to file\", \"parameters\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"write_file\", \"arguments\": {\"path\": \"output.txt\", \"content\": \"hello world\"}}]"}
|
|
29
|
+
{"instruction": "Run the tests", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"run_command\", \"description\": \"Run shell command\", \"parameters\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"run_command\", \"arguments\": {\"command\": \"npm test\"}}]"}
|
|
30
|
+
{"instruction": "List files in current directory", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"list_files\", \"description\": \"List files in directory\", \"parameters\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}}, \"required\": [\"path\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"list_files\", \"arguments\": {\"path\": \".\"}}]"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Demo: Fine-tune SigLIP2 for image classification
|
|
5
|
+
# This shows the full workflow with actual weight updates
|
|
6
|
+
|
|
7
|
+
require "bundler/setup"
|
|
8
|
+
require "fine"
|
|
9
|
+
|
|
10
|
+
puts "=" * 60
|
|
11
|
+
puts "FINE-TUNING DEMO"
|
|
12
|
+
puts "=" * 60
|
|
13
|
+
|
|
14
|
+
Fine.configure do |config|
|
|
15
|
+
config.progress_bar = false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
fixtures_path = File.expand_path("../spec/fixtures/images", __dir__)
|
|
19
|
+
|
|
20
|
+
puts "\n[1] Create and configure classifier"
|
|
21
|
+
classifier = Fine::ImageClassifier.new("google/siglip2-base-patch16-224") do |config|
|
|
22
|
+
config.epochs = 5
|
|
23
|
+
config.batch_size = 2
|
|
24
|
+
config.learning_rate = 1e-3 # Higher LR for faster learning on tiny dataset
|
|
25
|
+
config.freeze_encoder = true # Train only classification head
|
|
26
|
+
|
|
27
|
+
config.on_epoch_end do |epoch, metrics|
|
|
28
|
+
puts " Epoch #{epoch + 1}: loss=#{metrics[:loss].round(4)}, acc=#{(metrics[:accuracy] * 100).round(1)}%"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
puts "\n[2] Train on #{fixtures_path}"
|
|
33
|
+
puts " (4 images: 2 cats, 2 dogs)"
|
|
34
|
+
puts ""
|
|
35
|
+
|
|
36
|
+
history = classifier.fit(train_dir: fixtures_path)
|
|
37
|
+
|
|
38
|
+
puts "\n[3] Training metrics"
|
|
39
|
+
initial_loss = history.first[:loss]
|
|
40
|
+
final_loss = history.last[:loss]
|
|
41
|
+
initial_acc = history.first[:accuracy]
|
|
42
|
+
final_acc = history.last[:accuracy]
|
|
43
|
+
|
|
44
|
+
puts " Initial: loss=#{initial_loss.round(4)}, accuracy=#{(initial_acc * 100).round(1)}%"
|
|
45
|
+
puts " Final: loss=#{final_loss.round(4)}, accuracy=#{(final_acc * 100).round(1)}%"
|
|
46
|
+
|
|
47
|
+
if final_loss < initial_loss
|
|
48
|
+
puts " ✓ Loss decreased by #{((1 - final_loss/initial_loss) * 100).round(1)}%"
|
|
49
|
+
else
|
|
50
|
+
puts " ⚠ Loss did not decrease"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if final_acc > initial_acc
|
|
54
|
+
puts " ✓ Accuracy improved from #{(initial_acc * 100).round(1)}% to #{(final_acc * 100).round(1)}%"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
puts "\n[4] Test predictions"
|
|
58
|
+
Dir.glob(File.join(fixtures_path, "*/*.jpg")).each do |image_path|
|
|
59
|
+
true_label = File.basename(File.dirname(image_path))
|
|
60
|
+
predictions = classifier.predict(image_path).first
|
|
61
|
+
pred_label = predictions.first[:label]
|
|
62
|
+
pred_score = predictions.first[:score]
|
|
63
|
+
|
|
64
|
+
status = pred_label == true_label ? "✓" : "✗"
|
|
65
|
+
puts " #{status} #{File.basename(image_path)}: predicted=#{pred_label} (#{(pred_score * 100).round(1)}%), actual=#{true_label}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
puts "\n[5] Save and reload model"
|
|
69
|
+
save_path = "/tmp/fine_demo_model"
|
|
70
|
+
classifier.save(save_path)
|
|
71
|
+
puts " Saved to: #{save_path}"
|
|
72
|
+
|
|
73
|
+
loaded = Fine::ImageClassifier.load(save_path)
|
|
74
|
+
puts " Loaded successfully with #{loaded.label_map.size} classes: #{loaded.label_map.keys.join(', ')}"
|
|
75
|
+
|
|
76
|
+
puts "\n" + "=" * 60
|
|
77
|
+
puts "DEMO COMPLETE"
|
|
78
|
+
puts "=" * 60
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Fine-tune Gemma 3 (1B) for tool calling
|
|
5
|
+
#
|
|
6
|
+
# This script fine-tunes Gemma 3 1B instruction-tuned model to generate
|
|
7
|
+
# tool calls in Ollama-compatible format.
|
|
8
|
+
#
|
|
9
|
+
# Format:
|
|
10
|
+
# Input: [AVAILABLE_TOOLS] [{...tool definitions...}] [/AVAILABLE_TOOLS]
|
|
11
|
+
# Output: [TOOL_CALLS] [{...tool calls...}]
|
|
12
|
+
|
|
13
|
+
require "bundler/setup"
|
|
14
|
+
require "fine"
|
|
15
|
+
|
|
16
|
+
puts "=" * 60
|
|
17
|
+
puts "GEMMA 3 TOOL CALLING FINE-TUNING"
|
|
18
|
+
puts "=" * 60
|
|
19
|
+
|
|
20
|
+
Fine.configure do |config|
|
|
21
|
+
config.progress_bar = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
data_path = File.expand_path("data/tool_calls.jsonl", __dir__)
|
|
25
|
+
|
|
26
|
+
# Gemma 3 1B instruction-tuned
|
|
27
|
+
model_id = "google/gemma-3-1b-it"
|
|
28
|
+
|
|
29
|
+
puts "\n1. Setting up LLM with #{model_id}..."
|
|
30
|
+
puts " (This will download ~2GB from HuggingFace if not cached)"
|
|
31
|
+
|
|
32
|
+
begin
|
|
33
|
+
llm = Fine::LLM.new(model_id) do |config|
|
|
34
|
+
config.epochs = 3
|
|
35
|
+
config.batch_size = 1 # Reduced for memory
|
|
36
|
+
config.learning_rate = 2e-5
|
|
37
|
+
config.max_length = 256 # Reduced for memory
|
|
38
|
+
config.gradient_accumulation_steps = 1 # Set to 1 to avoid memory issues
|
|
39
|
+
config.warmup_steps = 10
|
|
40
|
+
config.max_grad_norm = nil # Disable gradient clipping to simplify
|
|
41
|
+
|
|
42
|
+
config.on_epoch_end do |epoch, metrics|
|
|
43
|
+
puts " Epoch #{epoch}: loss=#{metrics[:loss].round(4)}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
puts " Config:"
|
|
48
|
+
puts " Epochs: #{llm.config.epochs}"
|
|
49
|
+
puts " Batch size: #{llm.config.batch_size}"
|
|
50
|
+
puts " Learning rate: #{llm.config.learning_rate}"
|
|
51
|
+
puts " Max length: #{llm.config.max_length}"
|
|
52
|
+
|
|
53
|
+
puts "\n2. Loading training data from #{data_path}..."
|
|
54
|
+
line_count = File.readlines(data_path).count
|
|
55
|
+
puts " Found #{line_count} examples"
|
|
56
|
+
|
|
57
|
+
puts "\n3. Starting fine-tuning..."
|
|
58
|
+
puts " (This may take a while depending on your hardware)"
|
|
59
|
+
puts ""
|
|
60
|
+
|
|
61
|
+
history = llm.fit(train_file: data_path, format: :alpaca)
|
|
62
|
+
|
|
63
|
+
puts "\n4. Training completed!"
|
|
64
|
+
puts " Final loss: #{history.last[:loss].round(4)}"
|
|
65
|
+
|
|
66
|
+
if history.size >= 2 && history.last[:loss] < history.first[:loss]
|
|
67
|
+
improvement = ((1 - history.last[:loss] / history.first[:loss]) * 100).round(1)
|
|
68
|
+
puts " Loss improved by #{improvement}%"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
puts "\n5. Testing tool call generation..."
|
|
72
|
+
|
|
73
|
+
test_prompts = [
|
|
74
|
+
{
|
|
75
|
+
instruction: "What's the weather in Chicago?",
|
|
76
|
+
tools: "Available tools:\n- get_weather(location: string) - Get current weather\n- calculate(expression: string) - Evaluate math"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
instruction: "Calculate 50 + 25 * 2",
|
|
80
|
+
tools: "Available tools:\n- get_weather(location: string) - Get current weather\n- calculate(expression: string) - Evaluate math"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
instruction: "Search for Python tutorials",
|
|
84
|
+
tools: "Available tools:\n- search_web(query: string) - Search the web\n- get_weather(location: string) - Get weather"
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
test_prompts.each do |prompt|
|
|
89
|
+
full_prompt = "### Instruction:\n#{prompt[:instruction]}\n\n### Input:\n#{prompt[:tools]}\n\n### Response:\n"
|
|
90
|
+
|
|
91
|
+
response = llm.generate(
|
|
92
|
+
full_prompt,
|
|
93
|
+
max_new_tokens: 100,
|
|
94
|
+
temperature: 0.1, # Low temperature for deterministic tool calls
|
|
95
|
+
do_sample: false # Greedy decoding for consistent output
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Extract just the response part
|
|
99
|
+
generated = response.split("### Response:").last.strip
|
|
100
|
+
|
|
101
|
+
puts "\n Prompt: \"#{prompt[:instruction]}\""
|
|
102
|
+
puts " Generated:"
|
|
103
|
+
puts " #{generated.lines.first(5).map(&:strip).join("\n ")}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
puts "\n6. Saving fine-tuned model..."
|
|
107
|
+
save_path = "/tmp/gemma3-tool-calling"
|
|
108
|
+
llm.save(save_path)
|
|
109
|
+
puts " Saved to: #{save_path}"
|
|
110
|
+
|
|
111
|
+
puts "\n7. Testing load and generate..."
|
|
112
|
+
loaded = Fine::LLM.load(save_path)
|
|
113
|
+
|
|
114
|
+
test_prompt = "### Instruction:\nCheck the temperature in Boston\n\n### Input:\nAvailable tools:\n- get_weather(location: string) - Get weather\n\n### Response:\n"
|
|
115
|
+
loaded_response = loaded.generate(test_prompt, max_new_tokens: 50, do_sample: false)
|
|
116
|
+
generated = loaded_response.split("### Response:").last.strip
|
|
117
|
+
|
|
118
|
+
puts " Loaded model response:"
|
|
119
|
+
puts " #{generated.lines.first(3).map(&:strip).join("\n ")}"
|
|
120
|
+
|
|
121
|
+
puts "\n" + "=" * 60
|
|
122
|
+
puts "GEMMA 3 TOOL CALLING FINE-TUNING COMPLETE!"
|
|
123
|
+
puts "=" * 60
|
|
124
|
+
puts "\nModel saved to: #{save_path}"
|
|
125
|
+
puts "You can load it with: Fine::LLM.load('#{save_path}')"
|
|
126
|
+
|
|
127
|
+
rescue => e
|
|
128
|
+
puts "\n" + "=" * 60
|
|
129
|
+
puts "FINE-TUNING FAILED!"
|
|
130
|
+
puts "=" * 60
|
|
131
|
+
puts "\nError: #{e.class}: #{e.message}"
|
|
132
|
+
puts "\nBacktrace:"
|
|
133
|
+
puts e.backtrace.first(20).join("\n")
|
|
134
|
+
exit 1
|
|
135
|
+
end
|